mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Merge remote-tracking branch 'origin/master' into aakansha-detect-browser-zoom
This commit is contained in:
commit
c1f972179a
88 changed files with 2887 additions and 3226 deletions
|
@ -11,3 +11,12 @@ REACT_APP_WS_SERVER_URL=http://localhost:3002
|
||||||
REACT_APP_PORTAL_URL=
|
REACT_APP_PORTAL_URL=
|
||||||
|
|
||||||
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}'
|
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}'
|
||||||
|
|
||||||
|
# put these in your .env.local, or make sure you don't commit!
|
||||||
|
# must be lowercase `true` when turned on
|
||||||
|
#
|
||||||
|
# whether to enable Service Workers in development
|
||||||
|
REACT_APP_DEV_ENABLE_SW=
|
||||||
|
# whether to disable live reload / HMR. Usuaully what you want to do when
|
||||||
|
# debugging Service Workers.
|
||||||
|
REACT_APP_DEV_DISABLE_LIVE_RELOAD=
|
||||||
|
|
37
.github/dependabot.yml
vendored
37
.github/dependabot.yml
vendored
|
@ -1,37 +0,0 @@
|
||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: npm
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
day: sunday
|
|
||||||
time: "01:00"
|
|
||||||
reviewers:
|
|
||||||
- lipis
|
|
||||||
assignees:
|
|
||||||
- lipis
|
|
||||||
open-pull-requests-limit: 20
|
|
||||||
|
|
||||||
- package-ecosystem: npm
|
|
||||||
directory: /src/packages/excalidraw/
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
day: sunday
|
|
||||||
time: "01:00"
|
|
||||||
reviewers:
|
|
||||||
- ad1992
|
|
||||||
assignees:
|
|
||||||
- ad1992
|
|
||||||
open-pull-requests-limit: 20
|
|
||||||
|
|
||||||
- package-ecosystem: npm
|
|
||||||
directory: /src/packages/utils/
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
day: sunday
|
|
||||||
time: "01:00"
|
|
||||||
reviewers:
|
|
||||||
- ad1992
|
|
||||||
assignees:
|
|
||||||
- ad1992
|
|
||||||
open-pull-requests-limit: 20
|
|
2
.github/workflows/autorelease-excalidraw.yml
vendored
2
.github/workflows/autorelease-excalidraw.yml
vendored
|
@ -1,4 +1,4 @@
|
||||||
name: Auto release @excalidraw/excalidraw-next
|
name: Auto release excalidraw next
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
|
|
4
.github/workflows/autorelease-preview.yml
vendored
4
.github/workflows/autorelease-preview.yml
vendored
|
@ -1,4 +1,4 @@
|
||||||
name: Auto release preview @excalidraw/excalidraw-preview
|
name: Auto release excalidraw preview
|
||||||
on:
|
on:
|
||||||
issue_comment:
|
issue_comment:
|
||||||
types: [created, edited]
|
types: [created, edited]
|
||||||
|
@ -6,7 +6,7 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
Auto-release-excalidraw-preview:
|
Auto-release-excalidraw-preview:
|
||||||
name: Auto release preview
|
name: Auto release preview
|
||||||
if: github.event.comment.body == '@excalibot release package' && github.event.issue.pull_request
|
if: github.event.comment.body == '@excalibot trigger release' && github.event.issue.pull_request
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: React to release comment
|
- name: React to release comment
|
||||||
|
|
|
@ -94,7 +94,8 @@
|
||||||
"build:app:docker": "REACT_APP_DISABLE_SENTRY=true react-scripts build",
|
"build:app:docker": "REACT_APP_DISABLE_SENTRY=true react-scripts build",
|
||||||
"build:app": "REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts build",
|
"build:app": "REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts build",
|
||||||
"build:version": "node ./scripts/build-version.js",
|
"build:version": "node ./scripts/build-version.js",
|
||||||
"build": "yarn build:app && yarn build:version",
|
"build:prebuild": "node ./scripts/prebuild.js",
|
||||||
|
"build": "yarn build:prebuild && yarn build:app && yarn build:version",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
"fix:code": "yarn test:code --fix",
|
"fix:code": "yarn test:code --fix",
|
||||||
"fix:other": "yarn prettier --write",
|
"fix:other": "yarn prettier --write",
|
||||||
|
|
|
@ -98,6 +98,22 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<link rel="stylesheet" href="fonts.css" type="text/css" />
|
<link rel="stylesheet" href="fonts.css" type="text/css" />
|
||||||
|
<% if (process.env.REACT_APP_DEV_DISABLE_LIVE_RELOAD === "true") { %>
|
||||||
|
<script>
|
||||||
|
{
|
||||||
|
const _WebSocket = window.WebSocket;
|
||||||
|
window.WebSocket = function (url) {
|
||||||
|
if (/ws:\/\/localhost:.+?\/sockjs-node/.test(url)) {
|
||||||
|
console.info(
|
||||||
|
"[!!!] Live reload is disabled via process.env.REACT_APP_DEV_DISABLE_LIVE_RELOAD [!!!]",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return new _WebSocket(url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<% } %>
|
||||||
<script>
|
<script>
|
||||||
window.EXCALIDRAW_ASSET_PATH = "/";
|
window.EXCALIDRAW_ASSET_PATH = "/";
|
||||||
// setting this so that libraries installation reuses this window tab.
|
// setting this so that libraries installation reuses this window tab.
|
||||||
|
|
|
@ -17,11 +17,23 @@
|
||||||
* See https://goo.gl/2aRDsh
|
* See https://goo.gl/2aRDsh
|
||||||
*/
|
*/
|
||||||
|
|
||||||
importScripts("/workbox/workbox-sw.js");
|
// in dev, `process` is undefined because this file is not compiled until build
|
||||||
|
const IS_DEVELOPMENT =
|
||||||
|
typeof process === "undefined" || process.env.NODE_ENV !== "production";
|
||||||
|
|
||||||
workbox.setConfig({
|
if (IS_DEVELOPMENT) {
|
||||||
modulePathPrefix: "/workbox/",
|
importScripts(
|
||||||
});
|
"https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js",
|
||||||
|
);
|
||||||
|
workbox.setConfig({
|
||||||
|
debug: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
importScripts("/workbox/workbox-sw.js");
|
||||||
|
workbox.setConfig({
|
||||||
|
modulePathPrefix: "/workbox/",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
self.addEventListener("message", (event) => {
|
self.addEventListener("message", (event) => {
|
||||||
if (event.data && event.data.type === "SKIP_WAITING") {
|
if (event.data && event.data.type === "SKIP_WAITING") {
|
||||||
|
@ -30,14 +42,17 @@ self.addEventListener("message", (event) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
workbox.core.clientsClaim();
|
workbox.core.clientsClaim();
|
||||||
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
|
|
||||||
|
|
||||||
workbox.routing.registerNavigationRoute(
|
if (!IS_DEVELOPMENT) {
|
||||||
workbox.precaching.getCacheKeyForURL("./index.html"),
|
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
|
||||||
{
|
|
||||||
blacklist: [/^\/_/, /\/[^/?]+\.[^/]+$/],
|
workbox.routing.registerNavigationRoute(
|
||||||
},
|
workbox.precaching.getCacheKeyForURL("./index.html"),
|
||||||
);
|
{
|
||||||
|
blacklist: [/^\/_/, /\/[^/?]+\.[^/]+$/],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Cache relevant font files
|
// Cache relevant font files
|
||||||
workbox.routing.registerRoute(
|
workbox.routing.registerRoute(
|
|
@ -5,22 +5,25 @@ const core = require("@actions/core");
|
||||||
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
|
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
|
||||||
const excalidrawPackage = `${excalidrawDir}/package.json`;
|
const excalidrawPackage = `${excalidrawDir}/package.json`;
|
||||||
const pkg = require(excalidrawPackage);
|
const pkg = require(excalidrawPackage);
|
||||||
|
const isPreview = process.argv.slice(2)[0] === "preview";
|
||||||
|
|
||||||
const getShortCommitHash = () => {
|
const getShortCommitHash = () => {
|
||||||
return execSync("git rev-parse --short HEAD").toString().trim();
|
return execSync("git rev-parse --short HEAD").toString().trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
const publish = () => {
|
const publish = () => {
|
||||||
|
const tag = isPreview ? "preview" : "next";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
execSync(`yarn --frozen-lockfile`);
|
execSync(`yarn --frozen-lockfile`);
|
||||||
execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir });
|
execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir });
|
||||||
execSync(`yarn run build:umd`, { cwd: excalidrawDir });
|
execSync(`yarn run build:umd`, { cwd: excalidrawDir });
|
||||||
execSync(`yarn --cwd ${excalidrawDir} publish`);
|
execSync(`yarn --cwd ${excalidrawDir} publish --tag ${tag}`);
|
||||||
console.info("Published 🎉");
|
console.info(`Published ${pkg.name}@${tag}🎉`);
|
||||||
core.setOutput(
|
core.setOutput(
|
||||||
"result",
|
"result",
|
||||||
`**Preview version has been shipped** :rocket:
|
`**Preview version has been shipped** :rocket:
|
||||||
You can use [@excalidraw/excalidraw-preview@${pkg.version}](https://www.npmjs.com/package/@excalidraw/excalidraw-preview/v/${pkg.version}) for testing!`,
|
You can use [@excalidraw/excalidraw@${pkg.version}](https://www.npmjs.com/package/@excalidraw/excalidraw/v/${pkg.version}) for testing!`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
core.setOutput("result", "package couldn't be published :warning:!");
|
core.setOutput("result", "package couldn't be published :warning:!");
|
||||||
|
@ -51,27 +54,19 @@ exec(`git diff --name-only HEAD^ HEAD`, async (error, stdout, stderr) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// update package.json
|
// update package.json
|
||||||
pkg.name = "@excalidraw/excalidraw-next";
|
|
||||||
let version = `${pkg.version}-${getShortCommitHash()}`;
|
let version = `${pkg.version}-${getShortCommitHash()}`;
|
||||||
|
|
||||||
// update readme
|
// update readme
|
||||||
let data = fs.readFileSync(`${excalidrawDir}/README_NEXT.md`, "utf8");
|
|
||||||
|
|
||||||
const isPreview = process.argv.slice(2)[0] === "preview";
|
|
||||||
if (isPreview) {
|
if (isPreview) {
|
||||||
// use pullNumber-commithash as the version for preview
|
// use pullNumber-commithash as the version for preview
|
||||||
const pullRequestNumber = process.argv.slice(3)[0];
|
const pullRequestNumber = process.argv.slice(3)[0];
|
||||||
version = `${pkg.version}-${pullRequestNumber}-${getShortCommitHash()}`;
|
version = `${pkg.version}-${pullRequestNumber}-${getShortCommitHash()}`;
|
||||||
// replace "excalidraw-next" with "excalidraw-preview"
|
|
||||||
pkg.name = "@excalidraw/excalidraw-preview";
|
|
||||||
data = data.replace(/excalidraw-next/g, "excalidraw-preview");
|
|
||||||
data = data.trim();
|
|
||||||
}
|
}
|
||||||
pkg.version = version;
|
pkg.version = version;
|
||||||
|
|
||||||
fs.writeFileSync(excalidrawPackage, JSON.stringify(pkg, null, 2), "utf8");
|
fs.writeFileSync(excalidrawPackage, JSON.stringify(pkg, null, 2), "utf8");
|
||||||
|
|
||||||
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
|
|
||||||
console.info("Publish in progress...");
|
console.info("Publish in progress...");
|
||||||
publish();
|
publish();
|
||||||
});
|
});
|
||||||
|
|
20
scripts/prebuild.js
Normal file
20
scripts/prebuild.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
// for development purposes we want to have the service-worker.js file
|
||||||
|
// accessible from the public folder. On build though, we need to compile it
|
||||||
|
// and CRA expects that file to be in src/ folder.
|
||||||
|
const moveServiceWorkerScript = () => {
|
||||||
|
const oldPath = "./public/service-worker.js";
|
||||||
|
const newPath = "./src/service-worker.js";
|
||||||
|
|
||||||
|
fs.rename(oldPath, newPath, (error) => {
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
console.info("public/service-worker.js moved to src/");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
moveServiceWorkerScript();
|
|
@ -1,11 +1,21 @@
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const util = require("util");
|
const util = require("util");
|
||||||
const exec = util.promisify(require("child_process").exec);
|
const exec = util.promisify(require("child_process").exec);
|
||||||
const updateReadme = require("./updateReadme");
|
|
||||||
const updateChangelog = require("./updateChangelog");
|
const updateChangelog = require("./updateChangelog");
|
||||||
|
|
||||||
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
|
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
|
||||||
const excalidrawPackage = `${excalidrawDir}/package.json`;
|
const excalidrawPackage = `${excalidrawDir}/package.json`;
|
||||||
|
const originalReadMe = fs.readFileSync(`${excalidrawDir}/README.md`, "utf8");
|
||||||
|
|
||||||
|
const updateReadme = () => {
|
||||||
|
const excalidrawIndex = originalReadMe.indexOf("### Excalidraw");
|
||||||
|
|
||||||
|
// remove note for stable readme
|
||||||
|
const data = originalReadMe.slice(excalidrawIndex);
|
||||||
|
|
||||||
|
// update readme
|
||||||
|
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
|
||||||
|
};
|
||||||
|
|
||||||
const updatePackageVersion = (nextVersion) => {
|
const updatePackageVersion = (nextVersion) => {
|
||||||
const pkg = require(excalidrawPackage);
|
const pkg = require(excalidrawPackage);
|
||||||
|
@ -23,8 +33,10 @@ const release = async (nextVersion) => {
|
||||||
await exec(
|
await exec(
|
||||||
`git commit -m "docs: release @excalidraw/excalidraw@${nextVersion} 🎉"`,
|
`git commit -m "docs: release @excalidraw/excalidraw@${nextVersion} 🎉"`,
|
||||||
);
|
);
|
||||||
/* eslint-disable no-console */
|
// revert readme after release
|
||||||
console.log("Done!");
|
fs.writeFileSync(`${excalidrawDir}/README.md`, originalReadMe, "utf8");
|
||||||
|
|
||||||
|
console.info("Done!");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const updateReadme = () => {
|
|
||||||
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
|
|
||||||
let data = fs.readFileSync(`${excalidrawDir}/README_NEXT.md`, "utf8");
|
|
||||||
|
|
||||||
// remove note for unstable release
|
|
||||||
data = data.replace(
|
|
||||||
/<!-- unstable-readme-start-->[\s\S]*?<!-- unstable-readme-end-->/,
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
|
|
||||||
// replace "excalidraw-next" with "excalidraw"
|
|
||||||
data = data.replace(/excalidraw-next/g, "excalidraw");
|
|
||||||
data = data.trim();
|
|
||||||
|
|
||||||
const demoIndex = data.indexOf("### Demo");
|
|
||||||
const excalidrawNextNote =
|
|
||||||
"#### Note\n\n**If you don't want to wait for the next stable release and try out the unreleased changes you can use [@excalidraw/excalidraw-next](https://www.npmjs.com/package/@excalidraw/excalidraw-next).**\n\n";
|
|
||||||
// Add excalidraw next note to try out for unreleased changes
|
|
||||||
data = data.slice(0, demoIndex) + excalidrawNextNote + data.slice(demoIndex);
|
|
||||||
|
|
||||||
// update readme
|
|
||||||
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = updateReadme;
|
|
|
@ -7,7 +7,7 @@ import { DarkModeToggle } from "../components/DarkModeToggle";
|
||||||
import { loadFromJSON, saveAsJSON } from "../data";
|
import { loadFromJSON, saveAsJSON } from "../data";
|
||||||
import { resaveAsImageWithScene } from "../data/resave";
|
import { resaveAsImageWithScene } from "../data/resave";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useDeviceType } from "../components/App";
|
import { useDevice } from "../components/App";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { CheckboxItem } from "../components/CheckboxItem";
|
import { CheckboxItem } from "../components/CheckboxItem";
|
||||||
|
@ -204,7 +204,7 @@ export const actionSaveFileToDisk = register({
|
||||||
icon={saveAs}
|
icon={saveAs}
|
||||||
title={t("buttons.saveAs")}
|
title={t("buttons.saveAs")}
|
||||||
aria-label={t("buttons.saveAs")}
|
aria-label={t("buttons.saveAs")}
|
||||||
showAriaLabel={useDeviceType().isMobile}
|
showAriaLabel={useDevice().isMobile}
|
||||||
hidden={!nativeFileSystemSupported}
|
hidden={!nativeFileSystemSupported}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
data-testid="save-as-button"
|
data-testid="save-as-button"
|
||||||
|
@ -248,7 +248,7 @@ export const actionLoadScene = register({
|
||||||
icon={load}
|
icon={load}
|
||||||
title={t("buttons.load")}
|
title={t("buttons.load")}
|
||||||
aria-label={t("buttons.load")}
|
aria-label={t("buttons.load")}
|
||||||
showAriaLabel={useDeviceType().isMobile}
|
showAriaLabel={useDevice().isMobile}
|
||||||
onClick={updateData}
|
onClick={updateData}
|
||||||
data-testid="load-button"
|
data-testid="load-button"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { AppState } from "../types";
|
||||||
export const actionFinalize = register({
|
export const actionFinalize = register({
|
||||||
name: "finalize",
|
name: "finalize",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState, _, { canvas, focusContainer }) => {
|
perform: (elements, appState, _, { canvas, focusContainer, scene }) => {
|
||||||
if (appState.editingLinearElement) {
|
if (appState.editingLinearElement) {
|
||||||
const { elementId, startBindingElement, endBindingElement } =
|
const { elementId, startBindingElement, endBindingElement } =
|
||||||
appState.editingLinearElement;
|
appState.editingLinearElement;
|
||||||
|
@ -50,8 +50,12 @@ export const actionFinalize = register({
|
||||||
|
|
||||||
let newElements = elements;
|
let newElements = elements;
|
||||||
|
|
||||||
if (appState.pendingImageElement) {
|
const pendingImageElement =
|
||||||
mutateElement(appState.pendingImageElement, { isDeleted: true }, false);
|
appState.pendingImageElementId &&
|
||||||
|
scene.getElement(appState.pendingImageElementId);
|
||||||
|
|
||||||
|
if (pendingImageElement) {
|
||||||
|
mutateElement(pendingImageElement, { isDeleted: true }, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.document.activeElement instanceof HTMLElement) {
|
if (window.document.activeElement instanceof HTMLElement) {
|
||||||
|
@ -177,7 +181,7 @@ export const actionFinalize = register({
|
||||||
[multiPointElement.id]: true,
|
[multiPointElement.id]: true,
|
||||||
}
|
}
|
||||||
: appState.selectedElementIds,
|
: appState.selectedElementIds,
|
||||||
pendingImageElement: null,
|
pendingImageElementId: null,
|
||||||
},
|
},
|
||||||
commitToHistory: appState.activeTool.type === "freedraw",
|
commitToHistory: appState.activeTool.type === "freedraw",
|
||||||
};
|
};
|
||||||
|
|
|
@ -31,16 +31,7 @@ export const actionGoToCollaborator = register({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ appState, updateData, data }) => {
|
PanelComponent: ({ appState, updateData, data }) => {
|
||||||
const clientId: string | undefined = data?.id;
|
const [clientId, collaborator] = data as [string, Collaborator];
|
||||||
if (!clientId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const collaborator = appState.collaborators.get(clientId);
|
|
||||||
|
|
||||||
if (!collaborator) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { background, stroke } = getClientColors(clientId, appState);
|
const { background, stroke } = getClientColors(clientId, appState);
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,7 @@ describe("actionStyles", () => {
|
||||||
Keyboard.withModifierKeys({ ctrl: true, alt: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true, alt: true }, () => {
|
||||||
Keyboard.codeDown(CODES.C);
|
Keyboard.codeDown(CODES.C);
|
||||||
});
|
});
|
||||||
const secondRect = JSON.parse(copiedStyles);
|
const secondRect = JSON.parse(copiedStyles)[0];
|
||||||
expect(secondRect.id).toBe(h.elements[1].id);
|
expect(secondRect.id).toBe(h.elements[1].id);
|
||||||
|
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
|
|
|
@ -6,13 +6,15 @@ import {
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import {
|
import {
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_TEXT_ALIGN,
|
DEFAULT_TEXT_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { getContainerElement } from "../element/textElement";
|
import { getBoundTextElement } from "../element/textElement";
|
||||||
|
import { hasBoundTextElement } from "../element/typeChecks";
|
||||||
|
import { getSelectedElements } from "../scene";
|
||||||
|
|
||||||
// `copiedStyles` is exported only for tests.
|
// `copiedStyles` is exported only for tests.
|
||||||
export let copiedStyles: string = "{}";
|
export let copiedStyles: string = "{}";
|
||||||
|
@ -21,9 +23,15 @@ export const actionCopyStyles = register({
|
||||||
name: "copyStyles",
|
name: "copyStyles",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
|
const elementsCopied = [];
|
||||||
const element = elements.find((el) => appState.selectedElementIds[el.id]);
|
const element = elements.find((el) => appState.selectedElementIds[el.id]);
|
||||||
|
elementsCopied.push(element);
|
||||||
|
if (element && hasBoundTextElement(element)) {
|
||||||
|
const boundTextElement = getBoundTextElement(element);
|
||||||
|
elementsCopied.push(boundTextElement);
|
||||||
|
}
|
||||||
if (element) {
|
if (element) {
|
||||||
copiedStyles = JSON.stringify(element);
|
copiedStyles = JSON.stringify(elementsCopied);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
|
@ -42,31 +50,62 @@ export const actionPasteStyles = register({
|
||||||
name: "pasteStyles",
|
name: "pasteStyles",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
const pastedElement = JSON.parse(copiedStyles);
|
const elementsCopied = JSON.parse(copiedStyles);
|
||||||
|
const pastedElement = elementsCopied[0];
|
||||||
|
const boundTextElement = elementsCopied[1];
|
||||||
if (!isExcalidrawElement(pastedElement)) {
|
if (!isExcalidrawElement(pastedElement)) {
|
||||||
return { elements, commitToHistory: false };
|
return { elements, commitToHistory: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectedElements = getSelectedElements(elements, appState, true);
|
||||||
|
const selectedElementIds = selectedElements.map((element) => element.id);
|
||||||
return {
|
return {
|
||||||
elements: elements.map((element) => {
|
elements: elements.map((element) => {
|
||||||
if (appState.selectedElementIds[element.id]) {
|
if (selectedElementIds.includes(element.id)) {
|
||||||
const newElement = newElementWith(element, {
|
let elementStylesToCopyFrom = pastedElement;
|
||||||
backgroundColor: pastedElement?.backgroundColor,
|
if (isTextElement(element) && element.containerId) {
|
||||||
strokeWidth: pastedElement?.strokeWidth,
|
elementStylesToCopyFrom = boundTextElement;
|
||||||
strokeColor: pastedElement?.strokeColor,
|
|
||||||
strokeStyle: pastedElement?.strokeStyle,
|
|
||||||
fillStyle: pastedElement?.fillStyle,
|
|
||||||
opacity: pastedElement?.opacity,
|
|
||||||
roughness: pastedElement?.roughness,
|
|
||||||
});
|
|
||||||
if (isTextElement(newElement) && isTextElement(element)) {
|
|
||||||
mutateElement(newElement, {
|
|
||||||
fontSize: pastedElement?.fontSize || DEFAULT_FONT_SIZE,
|
|
||||||
fontFamily: pastedElement?.fontFamily || DEFAULT_FONT_FAMILY,
|
|
||||||
textAlign: pastedElement?.textAlign || DEFAULT_TEXT_ALIGN,
|
|
||||||
});
|
|
||||||
|
|
||||||
redrawTextBoundingBox(newElement, getContainerElement(newElement));
|
|
||||||
}
|
}
|
||||||
|
if (!elementStylesToCopyFrom) {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
let newElement = newElementWith(element, {
|
||||||
|
backgroundColor: elementStylesToCopyFrom?.backgroundColor,
|
||||||
|
strokeWidth: elementStylesToCopyFrom?.strokeWidth,
|
||||||
|
strokeColor: elementStylesToCopyFrom?.strokeColor,
|
||||||
|
strokeStyle: elementStylesToCopyFrom?.strokeStyle,
|
||||||
|
fillStyle: elementStylesToCopyFrom?.fillStyle,
|
||||||
|
opacity: elementStylesToCopyFrom?.opacity,
|
||||||
|
roughness: elementStylesToCopyFrom?.roughness,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isTextElement(newElement)) {
|
||||||
|
newElement = newElementWith(newElement, {
|
||||||
|
fontSize: elementStylesToCopyFrom?.fontSize || DEFAULT_FONT_SIZE,
|
||||||
|
fontFamily:
|
||||||
|
elementStylesToCopyFrom?.fontFamily || DEFAULT_FONT_FAMILY,
|
||||||
|
textAlign:
|
||||||
|
elementStylesToCopyFrom?.textAlign || DEFAULT_TEXT_ALIGN,
|
||||||
|
});
|
||||||
|
let container = null;
|
||||||
|
if (newElement.containerId) {
|
||||||
|
container =
|
||||||
|
selectedElements.find(
|
||||||
|
(element) =>
|
||||||
|
isTextElement(newElement) &&
|
||||||
|
element.id === newElement.containerId,
|
||||||
|
) || null;
|
||||||
|
}
|
||||||
|
redrawTextBoundingBox(newElement, container);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newElement.type === "arrow") {
|
||||||
|
newElement = newElementWith(newElement, {
|
||||||
|
startArrowhead: elementStylesToCopyFrom.startArrowhead,
|
||||||
|
endArrowhead: elementStylesToCopyFrom.endArrowhead,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return newElement;
|
return newElement;
|
||||||
}
|
}
|
||||||
return element;
|
return element;
|
||||||
|
|
|
@ -30,7 +30,7 @@ const trackAction = (
|
||||||
trackEvent(
|
trackEvent(
|
||||||
action.trackEvent.category,
|
action.trackEvent.category,
|
||||||
action.trackEvent.action || action.name,
|
action.trackEvent.action || action.name,
|
||||||
`${source} (${app.deviceType.isMobile ? "mobile" : "desktop"})`,
|
`${source} (${app.device.isMobile ? "mobile" : "desktop"})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import {
|
||||||
ExcalidrawProps,
|
ExcalidrawProps,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { ToolButtonSize } from "../components/ToolButton";
|
|
||||||
|
|
||||||
export type ActionSource = "ui" | "keyboard" | "contextMenu" | "api";
|
export type ActionSource = "ui" | "keyboard" | "contextMenu" | "api";
|
||||||
|
|
||||||
|
@ -119,7 +118,7 @@ export type PanelComponentProps = {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
updateData: (formData?: any) => void;
|
updateData: (formData?: any) => void;
|
||||||
appProps: ExcalidrawProps;
|
appProps: ExcalidrawProps;
|
||||||
data?: Partial<{ id: string; size: ToolButtonSize }>;
|
data?: Record<string, any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
|
|
|
@ -58,6 +58,7 @@ export const getDefaultAppState = (): Omit<
|
||||||
gridSize: null,
|
gridSize: null,
|
||||||
isBindingEnabled: true,
|
isBindingEnabled: true,
|
||||||
isLibraryOpen: false,
|
isLibraryOpen: false,
|
||||||
|
isLibraryMenuDocked: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isResizing: false,
|
isResizing: false,
|
||||||
isRotating: false,
|
isRotating: false,
|
||||||
|
@ -87,7 +88,7 @@ export const getDefaultAppState = (): Omit<
|
||||||
value: 1 as NormalizedZoomValue,
|
value: 1 as NormalizedZoomValue,
|
||||||
},
|
},
|
||||||
viewModeEnabled: false,
|
viewModeEnabled: false,
|
||||||
pendingImageElement: null,
|
pendingImageElementId: null,
|
||||||
showHyperlinkPopup: false,
|
showHyperlinkPopup: false,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -146,7 +147,8 @@ const APP_STATE_STORAGE_CONF = (<
|
||||||
gridSize: { browser: true, export: true, server: true },
|
gridSize: { browser: true, export: true, server: true },
|
||||||
height: { browser: false, export: false, server: false },
|
height: { browser: false, export: false, server: false },
|
||||||
isBindingEnabled: { browser: false, export: false, server: false },
|
isBindingEnabled: { browser: false, export: false, server: false },
|
||||||
isLibraryOpen: { browser: false, export: false, server: false },
|
isLibraryOpen: { browser: true, export: false, server: false },
|
||||||
|
isLibraryMenuDocked: { browser: true, export: false, server: false },
|
||||||
isLoading: { browser: false, export: false, server: false },
|
isLoading: { browser: false, export: false, server: false },
|
||||||
isResizing: { browser: false, export: false, server: false },
|
isResizing: { browser: false, export: false, server: false },
|
||||||
isRotating: { browser: false, export: false, server: false },
|
isRotating: { browser: false, export: false, server: false },
|
||||||
|
@ -177,7 +179,7 @@ const APP_STATE_STORAGE_CONF = (<
|
||||||
zenModeEnabled: { browser: true, export: false, server: false },
|
zenModeEnabled: { browser: true, export: false, server: false },
|
||||||
zoom: { browser: true, export: false, server: false },
|
zoom: { browser: true, export: false, server: false },
|
||||||
viewModeEnabled: { browser: false, export: false, server: false },
|
viewModeEnabled: { browser: false, export: false, server: false },
|
||||||
pendingImageElement: { browser: false, export: false, server: false },
|
pendingImageElementId: { browser: false, export: false, server: false },
|
||||||
showHyperlinkPopup: { browser: false, export: false, server: false },
|
showHyperlinkPopup: { browser: false, export: false, server: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { ActionManager } from "../actions/manager";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { ExcalidrawElement, PointerType } from "../element/types";
|
import { ExcalidrawElement, PointerType } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useDeviceType } from "../components/App";
|
import { useDevice } from "../components/App";
|
||||||
import {
|
import {
|
||||||
canChangeSharpness,
|
canChangeSharpness,
|
||||||
canHaveArrowheads,
|
canHaveArrowheads,
|
||||||
|
@ -52,7 +52,7 @@ export const SelectedShapeActions = ({
|
||||||
isSingleElementBoundContainer = true;
|
isSingleElementBoundContainer = true;
|
||||||
}
|
}
|
||||||
const isEditing = Boolean(appState.editingElement);
|
const isEditing = Boolean(appState.editingElement);
|
||||||
const deviceType = useDeviceType();
|
const device = useDevice();
|
||||||
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
||||||
|
|
||||||
const showFillIcons =
|
const showFillIcons =
|
||||||
|
@ -177,8 +177,8 @@ export const SelectedShapeActions = ({
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.actions")}</legend>
|
<legend>{t("labels.actions")}</legend>
|
||||||
<div className="buttonList">
|
<div className="buttonList">
|
||||||
{!deviceType.isMobile && renderAction("duplicateSelection")}
|
{!device.isMobile && renderAction("duplicateSelection")}
|
||||||
{!deviceType.isMobile && renderAction("deleteSelectedElements")}
|
{!device.isMobile && renderAction("deleteSelectedElements")}
|
||||||
{renderAction("group")}
|
{renderAction("group")}
|
||||||
{renderAction("ungroup")}
|
{renderAction("ungroup")}
|
||||||
{showLinkIcon && renderAction("hyperlink")}
|
{showLinkIcon && renderAction("hyperlink")}
|
||||||
|
|
|
@ -64,6 +64,8 @@ import {
|
||||||
MQ_MAX_HEIGHT_LANDSCAPE,
|
MQ_MAX_HEIGHT_LANDSCAPE,
|
||||||
MQ_MAX_WIDTH_LANDSCAPE,
|
MQ_MAX_WIDTH_LANDSCAPE,
|
||||||
MQ_MAX_WIDTH_PORTRAIT,
|
MQ_MAX_WIDTH_PORTRAIT,
|
||||||
|
MQ_RIGHT_SIDEBAR_MIN_WIDTH,
|
||||||
|
MQ_SM_MAX_WIDTH,
|
||||||
POINTER_BUTTON,
|
POINTER_BUTTON,
|
||||||
SCROLL_TIMEOUT,
|
SCROLL_TIMEOUT,
|
||||||
TAP_TWICE_TIMEOUT,
|
TAP_TWICE_TIMEOUT,
|
||||||
|
@ -194,7 +196,7 @@ import {
|
||||||
LibraryItems,
|
LibraryItems,
|
||||||
PointerDownState,
|
PointerDownState,
|
||||||
SceneData,
|
SceneData,
|
||||||
DeviceType,
|
Device,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import {
|
import {
|
||||||
debounce,
|
debounce,
|
||||||
|
@ -220,7 +222,6 @@ import {
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import ContextMenu, { ContextMenuOption } from "./ContextMenu";
|
import ContextMenu, { ContextMenuOption } from "./ContextMenu";
|
||||||
import LayerUI from "./LayerUI";
|
import LayerUI from "./LayerUI";
|
||||||
import { Stats } from "./Stats";
|
|
||||||
import { Toast } from "./Toast";
|
import { Toast } from "./Toast";
|
||||||
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
|
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
|
||||||
import {
|
import {
|
||||||
|
@ -259,12 +260,14 @@ import {
|
||||||
isLocalLink,
|
isLocalLink,
|
||||||
} from "../element/Hyperlink";
|
} from "../element/Hyperlink";
|
||||||
|
|
||||||
const defaultDeviceTypeContext: DeviceType = {
|
const deviceContextInitialValue = {
|
||||||
|
isSmScreen: false,
|
||||||
isMobile: false,
|
isMobile: false,
|
||||||
isTouchScreen: false,
|
isTouchScreen: false,
|
||||||
|
canDeviceFitSidebar: false,
|
||||||
};
|
};
|
||||||
const DeviceTypeContext = React.createContext(defaultDeviceTypeContext);
|
const DeviceContext = React.createContext<Device>(deviceContextInitialValue);
|
||||||
export const useDeviceType = () => useContext(DeviceTypeContext);
|
export const useDevice = () => useContext<Device>(DeviceContext);
|
||||||
const ExcalidrawContainerContext = React.createContext<{
|
const ExcalidrawContainerContext = React.createContext<{
|
||||||
container: HTMLDivElement | null;
|
container: HTMLDivElement | null;
|
||||||
id: string | null;
|
id: string | null;
|
||||||
|
@ -296,10 +299,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
rc: RoughCanvas | null = null;
|
rc: RoughCanvas | null = null;
|
||||||
unmounted: boolean = false;
|
unmounted: boolean = false;
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
deviceType: DeviceType = {
|
device: Device = deviceContextInitialValue;
|
||||||
isMobile: false,
|
|
||||||
isTouchScreen: false,
|
|
||||||
};
|
|
||||||
detachIsMobileMqHandler?: () => void;
|
detachIsMobileMqHandler?: () => void;
|
||||||
|
|
||||||
private excalidrawContainerRef = React.createRef<HTMLDivElement>();
|
private excalidrawContainerRef = React.createRef<HTMLDivElement>();
|
||||||
|
@ -309,7 +309,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
UIOptions: DEFAULT_UI_OPTIONS,
|
UIOptions: DEFAULT_UI_OPTIONS,
|
||||||
};
|
};
|
||||||
|
|
||||||
private scene: Scene;
|
public scene: Scene;
|
||||||
private resizeObserver: ResizeObserver | undefined;
|
private resizeObserver: ResizeObserver | undefined;
|
||||||
private nearestScrollableContainer: HTMLElement | Document | undefined;
|
private nearestScrollableContainer: HTMLElement | Document | undefined;
|
||||||
public library: AppClassProperties["library"];
|
public library: AppClassProperties["library"];
|
||||||
|
@ -353,12 +353,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
height: window.innerHeight,
|
height: window.innerHeight,
|
||||||
showHyperlinkPopup: false,
|
showHyperlinkPopup: false,
|
||||||
|
isLibraryMenuDocked: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.id = nanoid();
|
this.id = nanoid();
|
||||||
|
|
||||||
this.library = new Library(this);
|
this.library = new Library(this);
|
||||||
|
|
||||||
if (excalidrawRef) {
|
if (excalidrawRef) {
|
||||||
const readyPromise =
|
const readyPromise =
|
||||||
("current" in excalidrawRef && excalidrawRef.current?.readyPromise) ||
|
("current" in excalidrawRef && excalidrawRef.current?.readyPromise) ||
|
||||||
|
@ -485,7 +485,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
<div
|
<div
|
||||||
className={clsx("excalidraw excalidraw-container", {
|
className={clsx("excalidraw excalidraw-container", {
|
||||||
"excalidraw--view-mode": viewModeEnabled,
|
"excalidraw--view-mode": viewModeEnabled,
|
||||||
"excalidraw--mobile": this.deviceType.isMobile,
|
"excalidraw--mobile": this.device.isMobile,
|
||||||
})}
|
})}
|
||||||
ref={this.excalidrawContainerRef}
|
ref={this.excalidrawContainerRef}
|
||||||
onDrop={this.handleAppOnDrop}
|
onDrop={this.handleAppOnDrop}
|
||||||
|
@ -497,7 +497,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
<ExcalidrawContainerContext.Provider
|
<ExcalidrawContainerContext.Provider
|
||||||
value={this.excalidrawContainerValue}
|
value={this.excalidrawContainerValue}
|
||||||
>
|
>
|
||||||
<DeviceTypeContext.Provider value={this.deviceType}>
|
<DeviceContext.Provider value={this.device}>
|
||||||
<LayerUI
|
<LayerUI
|
||||||
canvas={this.canvas}
|
canvas={this.canvas}
|
||||||
appState={this.state}
|
appState={this.state}
|
||||||
|
@ -521,6 +521,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
isCollaborating={this.props.isCollaborating}
|
isCollaborating={this.props.isCollaborating}
|
||||||
renderTopRightUI={renderTopRightUI}
|
renderTopRightUI={renderTopRightUI}
|
||||||
renderCustomFooter={renderFooter}
|
renderCustomFooter={renderFooter}
|
||||||
|
renderCustomStats={renderCustomStats}
|
||||||
viewModeEnabled={viewModeEnabled}
|
viewModeEnabled={viewModeEnabled}
|
||||||
showExitZenModeBtn={
|
showExitZenModeBtn={
|
||||||
typeof this.props?.zenModeEnabled === "undefined" &&
|
typeof this.props?.zenModeEnabled === "undefined" &&
|
||||||
|
@ -548,15 +549,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
onLinkOpen={this.props.onLinkOpen}
|
onLinkOpen={this.props.onLinkOpen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{this.state.showStats && (
|
|
||||||
<Stats
|
|
||||||
appState={this.state}
|
|
||||||
setAppState={this.setAppState}
|
|
||||||
elements={this.scene.getNonDeletedElements()}
|
|
||||||
onClose={this.toggleStats}
|
|
||||||
renderCustomStats={renderCustomStats}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{this.state.toastMessage !== null && (
|
{this.state.toastMessage !== null && (
|
||||||
<Toast
|
<Toast
|
||||||
message={this.state.toastMessage}
|
message={this.state.toastMessage}
|
||||||
|
@ -564,7 +556,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<main>{this.renderCanvas()}</main>
|
<main>{this.renderCanvas()}</main>
|
||||||
</DeviceTypeContext.Provider>
|
</DeviceContext.Provider>
|
||||||
</ExcalidrawContainerContext.Provider>
|
</ExcalidrawContainerContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -763,7 +755,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
const scene = restore(initialData, null, null);
|
const scene = restore(initialData, null, null);
|
||||||
scene.appState = {
|
scene.appState = {
|
||||||
...scene.appState,
|
...scene.appState,
|
||||||
isLibraryOpen: this.state.isLibraryOpen,
|
// we're falling back to current (pre-init) state when deciding
|
||||||
|
// whether to open the library, to handle a case where we
|
||||||
|
// update the state outside of initialData (e.g. when loading the app
|
||||||
|
// with a library install link, which should auto-open the library)
|
||||||
|
isLibraryOpen:
|
||||||
|
initialData?.appState?.isLibraryOpen || this.state.isLibraryOpen,
|
||||||
activeTool:
|
activeTool:
|
||||||
scene.appState.activeTool.type === "image"
|
scene.appState.activeTool.type === "image"
|
||||||
? { ...scene.appState.activeTool, type: "selection" }
|
? { ...scene.appState.activeTool, type: "selection" }
|
||||||
|
@ -794,6 +791,21 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private refreshDeviceState = (container: HTMLDivElement) => {
|
||||||
|
const { width, height } = container.getBoundingClientRect();
|
||||||
|
const sidebarBreakpoint =
|
||||||
|
this.props.UIOptions.dockedSidebarBreakpoint != null
|
||||||
|
? this.props.UIOptions.dockedSidebarBreakpoint
|
||||||
|
: MQ_RIGHT_SIDEBAR_MIN_WIDTH;
|
||||||
|
this.device = updateObject(this.device, {
|
||||||
|
isSmScreen: width < MQ_SM_MAX_WIDTH,
|
||||||
|
isMobile:
|
||||||
|
width < MQ_MAX_WIDTH_PORTRAIT ||
|
||||||
|
(height < MQ_MAX_HEIGHT_LANDSCAPE && width < MQ_MAX_WIDTH_LANDSCAPE),
|
||||||
|
canDeviceFitSidebar: width > sidebarBreakpoint,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
public async componentDidMount() {
|
public async componentDidMount() {
|
||||||
this.unmounted = false;
|
this.unmounted = false;
|
||||||
this.excalidrawContainerValue.container =
|
this.excalidrawContainerValue.container =
|
||||||
|
@ -835,34 +847,53 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.focusContainer();
|
this.focusContainer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.excalidrawContainerRef.current &&
|
||||||
|
// bounding rects don't work in tests so updating
|
||||||
|
// the state on init would result in making the test enviro run
|
||||||
|
// in mobile breakpoint (0 width/height), making everything fail
|
||||||
|
process.env.NODE_ENV !== "test"
|
||||||
|
) {
|
||||||
|
this.refreshDeviceState(this.excalidrawContainerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) {
|
if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) {
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
// compute isMobile state
|
// recompute device dimensions state
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
const { width, height } =
|
this.refreshDeviceState(this.excalidrawContainerRef.current!);
|
||||||
this.excalidrawContainerRef.current!.getBoundingClientRect();
|
|
||||||
this.deviceType = updateObject(this.deviceType, {
|
|
||||||
isMobile:
|
|
||||||
width < MQ_MAX_WIDTH_PORTRAIT ||
|
|
||||||
(height < MQ_MAX_HEIGHT_LANDSCAPE &&
|
|
||||||
width < MQ_MAX_WIDTH_LANDSCAPE),
|
|
||||||
});
|
|
||||||
// refresh offsets
|
// refresh offsets
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
this.updateDOMRect();
|
this.updateDOMRect();
|
||||||
});
|
});
|
||||||
this.resizeObserver?.observe(this.excalidrawContainerRef.current);
|
this.resizeObserver?.observe(this.excalidrawContainerRef.current);
|
||||||
} else if (window.matchMedia) {
|
} else if (window.matchMedia) {
|
||||||
const mediaQuery = window.matchMedia(
|
const mdScreenQuery = window.matchMedia(
|
||||||
`(max-width: ${MQ_MAX_WIDTH_PORTRAIT}px), (max-height: ${MQ_MAX_HEIGHT_LANDSCAPE}px) and (max-width: ${MQ_MAX_WIDTH_LANDSCAPE}px)`,
|
`(max-width: ${MQ_MAX_WIDTH_PORTRAIT}px), (max-height: ${MQ_MAX_HEIGHT_LANDSCAPE}px) and (max-width: ${MQ_MAX_WIDTH_LANDSCAPE}px)`,
|
||||||
);
|
);
|
||||||
|
const smScreenQuery = window.matchMedia(
|
||||||
|
`(max-width: ${MQ_SM_MAX_WIDTH}px)`,
|
||||||
|
);
|
||||||
|
const canDeviceFitSidebarMediaQuery = window.matchMedia(
|
||||||
|
`(min-width: ${
|
||||||
|
// NOTE this won't update if a different breakpoint is supplied
|
||||||
|
// after mount
|
||||||
|
this.props.UIOptions.dockedSidebarBreakpoint != null
|
||||||
|
? this.props.UIOptions.dockedSidebarBreakpoint
|
||||||
|
: MQ_RIGHT_SIDEBAR_MIN_WIDTH
|
||||||
|
}px)`,
|
||||||
|
);
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
this.deviceType = updateObject(this.deviceType, {
|
this.excalidrawContainerRef.current!.getBoundingClientRect();
|
||||||
isMobile: mediaQuery.matches,
|
this.device = updateObject(this.device, {
|
||||||
|
isSmScreen: smScreenQuery.matches,
|
||||||
|
isMobile: mdScreenQuery.matches,
|
||||||
|
canDeviceFitSidebar: canDeviceFitSidebarMediaQuery.matches,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
mediaQuery.addListener(handler);
|
mdScreenQuery.addListener(handler);
|
||||||
this.detachIsMobileMqHandler = () => mediaQuery.removeListener(handler);
|
this.detachIsMobileMqHandler = () =>
|
||||||
|
mdScreenQuery.removeListener(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(window.location.search.slice(1));
|
const searchParams = new URLSearchParams(window.location.search.slice(1));
|
||||||
|
@ -887,7 +918,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private onResize = withBatchedUpdates(() => {
|
private onResize = withBatchedUpdates(() => {
|
||||||
if (!this.deviceType.isMobile) {
|
if (!this.device.isMobile) {
|
||||||
const scrollBarWidth = 10;
|
const scrollBarWidth = 10;
|
||||||
const widthRatio =
|
const widthRatio =
|
||||||
(window.outerWidth - scrollBarWidth) / window.innerWidth;
|
(window.outerWidth - scrollBarWidth) / window.innerWidth;
|
||||||
|
@ -1016,6 +1047,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: AppProps, prevState: AppState) {
|
componentDidUpdate(prevProps: AppProps, prevState: AppState) {
|
||||||
|
if (
|
||||||
|
this.excalidrawContainerRef.current &&
|
||||||
|
prevProps.UIOptions.dockedSidebarBreakpoint !==
|
||||||
|
this.props.UIOptions.dockedSidebarBreakpoint
|
||||||
|
) {
|
||||||
|
this.refreshDeviceState(this.excalidrawContainerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
prevState.scrollX !== this.state.scrollX ||
|
prevState.scrollX !== this.state.scrollX ||
|
||||||
prevState.scrollY !== this.state.scrollY
|
prevState.scrollY !== this.state.scrollY
|
||||||
|
@ -1154,8 +1193,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
if (isImageElement(element)) {
|
if (isImageElement(element)) {
|
||||||
if (
|
if (
|
||||||
// not placed on canvas yet (but in elements array)
|
// not placed on canvas yet (but in elements array)
|
||||||
this.state.pendingImageElement &&
|
this.state.pendingImageElementId === element.id
|
||||||
element.id === this.state.pendingImageElement.id
|
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -1189,7 +1227,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
theme: this.state.theme,
|
theme: this.state.theme,
|
||||||
imageCache: this.imageCache,
|
imageCache: this.imageCache,
|
||||||
isExporting: false,
|
isExporting: false,
|
||||||
renderScrollbars: !this.deviceType.isMobile,
|
renderScrollbars: !this.device.isMobile,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1467,11 +1505,15 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
this.scene.replaceAllElements(nextElements);
|
this.scene.replaceAllElements(nextElements);
|
||||||
this.history.resumeRecording();
|
this.history.resumeRecording();
|
||||||
|
|
||||||
this.setState(
|
this.setState(
|
||||||
selectGroupsForSelectedElements(
|
selectGroupsForSelectedElements(
|
||||||
{
|
{
|
||||||
...this.state,
|
...this.state,
|
||||||
isLibraryOpen: false,
|
isLibraryOpen:
|
||||||
|
this.state.isLibraryOpen && this.device.canDeviceFitSidebar
|
||||||
|
? this.state.isLibraryMenuDocked
|
||||||
|
: false,
|
||||||
selectedElementIds: newElements.reduce((map, element) => {
|
selectedElementIds: newElements.reduce((map, element) => {
|
||||||
if (!isBoundToContainer(element)) {
|
if (!isBoundToContainer(element)) {
|
||||||
map[element.id] = true;
|
map[element.id] = true;
|
||||||
|
@ -1543,7 +1585,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
trackEvent(
|
trackEvent(
|
||||||
"toolbar",
|
"toolbar",
|
||||||
"toggleLock",
|
"toggleLock",
|
||||||
`${source} (${this.deviceType.isMobile ? "mobile" : "desktop"})`,
|
`${source} (${this.device.isMobile ? "mobile" : "desktop"})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.setState((prevState) => {
|
this.setState((prevState) => {
|
||||||
|
@ -1574,10 +1616,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.actionManager.executeAction(actionToggleZenMode);
|
this.actionManager.executeAction(actionToggleZenMode);
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleStats = () => {
|
|
||||||
this.actionManager.executeAction(actionToggleStats);
|
|
||||||
};
|
|
||||||
|
|
||||||
scrollToContent = (
|
scrollToContent = (
|
||||||
target:
|
target:
|
||||||
| ExcalidrawElement
|
| ExcalidrawElement
|
||||||
|
@ -1706,9 +1744,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bail if
|
||||||
if (
|
if (
|
||||||
(isWritableElement(event.target) && event.key !== KEYS.ESCAPE) ||
|
// inside an input
|
||||||
// case: using arrows to move between buttons
|
(isWritableElement(event.target) &&
|
||||||
|
// unless pressing escape (finalize action)
|
||||||
|
event.key !== KEYS.ESCAPE) ||
|
||||||
|
// or unless using arrows (to move between buttons)
|
||||||
(isArrowKey(event.key) && isInputLike(event.target))
|
(isArrowKey(event.key) && isInputLike(event.target))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
|
@ -1733,7 +1775,16 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.code === CODES.ZERO) {
|
if (event.code === CODES.ZERO) {
|
||||||
this.setState({ isLibraryOpen: !this.state.isLibraryOpen });
|
const nextState = !this.state.isLibraryOpen;
|
||||||
|
this.setState({ isLibraryOpen: nextState });
|
||||||
|
// track only openings
|
||||||
|
if (nextState) {
|
||||||
|
trackEvent(
|
||||||
|
"library",
|
||||||
|
"toggleLibrary (open)",
|
||||||
|
`keyboard (${this.device.isMobile ? "mobile" : "desktop"})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isArrowKey(event.key)) {
|
if (isArrowKey(event.key)) {
|
||||||
|
@ -1827,7 +1878,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
trackEvent(
|
trackEvent(
|
||||||
"toolbar",
|
"toolbar",
|
||||||
shape,
|
shape,
|
||||||
`keyboard (${this.deviceType.isMobile ? "mobile" : "desktop"})`,
|
`keyboard (${this.device.isMobile ? "mobile" : "desktop"})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.setActiveTool({ type: shape });
|
this.setActiveTool({ type: shape });
|
||||||
|
@ -2237,12 +2288,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
existingTextElement = selectedElements[0];
|
existingTextElement = selectedElements[0];
|
||||||
} else if (isTextBindableContainer(selectedElements[0], false)) {
|
} else if (isTextBindableContainer(selectedElements[0], false)) {
|
||||||
existingTextElement = getBoundTextElement(selectedElements[0]);
|
existingTextElement = getBoundTextElement(selectedElements[0]);
|
||||||
|
} else {
|
||||||
|
existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
|
||||||
}
|
}
|
||||||
|
|
||||||
existingTextElement =
|
|
||||||
existingTextElement ?? this.getTextElementAtPosition(sceneX, sceneY);
|
|
||||||
|
|
||||||
// bind to container when shouldBind is true or
|
// bind to container when shouldBind is true or
|
||||||
// clicked on center of container
|
// clicked on center of container
|
||||||
if (
|
if (
|
||||||
|
@ -2451,7 +2503,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
element,
|
element,
|
||||||
this.state,
|
this.state,
|
||||||
[scenePointer.x, scenePointer.y],
|
[scenePointer.x, scenePointer.y],
|
||||||
this.deviceType.isMobile,
|
this.device.isMobile,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -2483,7 +2535,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.hitLinkElement,
|
this.hitLinkElement,
|
||||||
this.state,
|
this.state,
|
||||||
[lastPointerDownCoords.x, lastPointerDownCoords.y],
|
[lastPointerDownCoords.x, lastPointerDownCoords.y],
|
||||||
this.deviceType.isMobile,
|
this.device.isMobile,
|
||||||
);
|
);
|
||||||
const lastPointerUpCoords = viewportCoordsToSceneCoords(
|
const lastPointerUpCoords = viewportCoordsToSceneCoords(
|
||||||
this.lastPointerUp!,
|
this.lastPointerUp!,
|
||||||
|
@ -2493,7 +2545,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.hitLinkElement,
|
this.hitLinkElement,
|
||||||
this.state,
|
this.state,
|
||||||
[lastPointerUpCoords.x, lastPointerUpCoords.y],
|
[lastPointerUpCoords.x, lastPointerUpCoords.y],
|
||||||
this.deviceType.isMobile,
|
this.device.isMobile,
|
||||||
);
|
);
|
||||||
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
|
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
|
||||||
const url = this.hitLinkElement.link;
|
const url = this.hitLinkElement.link;
|
||||||
|
@ -2932,10 +2984,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!this.deviceType.isTouchScreen &&
|
!this.device.isTouchScreen &&
|
||||||
["pen", "touch"].includes(event.pointerType)
|
["pen", "touch"].includes(event.pointerType)
|
||||||
) {
|
) {
|
||||||
this.deviceType = updateObject(this.deviceType, { isTouchScreen: true });
|
this.device = updateObject(this.device, { isTouchScreen: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPanning) {
|
if (isPanning) {
|
||||||
|
@ -3012,19 +3064,24 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
// reset image preview on pointerdown
|
// reset image preview on pointerdown
|
||||||
setCursor(this.canvas, CURSOR_TYPE.CROSSHAIR);
|
setCursor(this.canvas, CURSOR_TYPE.CROSSHAIR);
|
||||||
|
|
||||||
if (!this.state.pendingImageElement) {
|
// retrieve the latest element as the state may be stale
|
||||||
|
const pendingImageElement =
|
||||||
|
this.state.pendingImageElementId &&
|
||||||
|
this.scene.getElement(this.state.pendingImageElementId);
|
||||||
|
|
||||||
|
if (!pendingImageElement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
draggingElement: this.state.pendingImageElement,
|
draggingElement: pendingImageElement,
|
||||||
editingElement: this.state.pendingImageElement,
|
editingElement: pendingImageElement,
|
||||||
pendingImageElement: null,
|
pendingImageElementId: null,
|
||||||
multiElement: null,
|
multiElement: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { x, y } = viewportCoordsToSceneCoords(event, this.state);
|
const { x, y } = viewportCoordsToSceneCoords(event, this.state);
|
||||||
mutateElement(this.state.pendingImageElement, {
|
mutateElement(pendingImageElement, {
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
});
|
});
|
||||||
|
@ -3072,7 +3129,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
event: React.PointerEvent<HTMLCanvasElement>,
|
event: React.PointerEvent<HTMLCanvasElement>,
|
||||||
) => {
|
) => {
|
||||||
this.lastPointerUp = event;
|
this.lastPointerUp = event;
|
||||||
if (this.deviceType.isTouchScreen) {
|
if (this.device.isTouchScreen) {
|
||||||
const scenePointer = viewportCoordsToSceneCoords(
|
const scenePointer = viewportCoordsToSceneCoords(
|
||||||
{ clientX: event.clientX, clientY: event.clientY },
|
{ clientX: event.clientX, clientY: event.clientY },
|
||||||
this.state,
|
this.state,
|
||||||
|
@ -3090,7 +3147,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.hitLinkElement &&
|
this.hitLinkElement &&
|
||||||
!this.state.selectedElementIds[this.hitLinkElement.id]
|
!this.state.selectedElementIds[this.hitLinkElement.id]
|
||||||
) {
|
) {
|
||||||
this.redirectToLink(event, this.deviceType.isTouchScreen);
|
this.redirectToLink(event, this.device.isTouchScreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.removePointer(event);
|
this.removePointer(event);
|
||||||
|
@ -3462,7 +3519,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
pointerDownState.hit.element,
|
pointerDownState.hit.element,
|
||||||
this.state,
|
this.state,
|
||||||
[pointerDownState.origin.x, pointerDownState.origin.y],
|
[pointerDownState.origin.x, pointerDownState.origin.y],
|
||||||
this.deviceType.isMobile,
|
this.device.isMobile,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -4340,8 +4397,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
pointerDownState.eventListeners.onKeyUp!,
|
pointerDownState.eventListeners.onKeyUp!,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.state.pendingImageElement) {
|
if (this.state.pendingImageElementId) {
|
||||||
this.setState({ pendingImageElement: null });
|
this.setState({ pendingImageElementId: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (draggingElement?.type === "freedraw") {
|
if (draggingElement?.type === "freedraw") {
|
||||||
|
@ -4829,7 +4886,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
await cachedImageData.image;
|
await cachedImageData.image;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
this.state.pendingImageElement?.id !== imageElement.id &&
|
this.state.pendingImageElementId !== imageElement.id &&
|
||||||
this.state.draggingElement?.id !== imageElement.id
|
this.state.draggingElement?.id !== imageElement.id
|
||||||
) {
|
) {
|
||||||
this.initializeImageDimensions(imageElement, true);
|
this.initializeImageDimensions(imageElement, true);
|
||||||
|
@ -4911,7 +4968,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
previewDataURL = canvas.toDataURL(MIME_TYPES.svg) as DataURL;
|
previewDataURL = canvas.toDataURL(MIME_TYPES.svg) as DataURL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.pendingImageElement) {
|
if (this.state.pendingImageElementId) {
|
||||||
setCursor(this.canvas, `url(${previewDataURL}) 4 4, auto`);
|
setCursor(this.canvas, `url(${previewDataURL}) 4 4, auto`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -4952,7 +5009,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
} else {
|
} else {
|
||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
pendingImageElement: imageElement,
|
pendingImageElementId: imageElement.id,
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
this.insertImageElement(
|
this.insertImageElement(
|
||||||
|
@ -4971,7 +5028,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}
|
}
|
||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
pendingImageElement: null,
|
pendingImageElementId: null,
|
||||||
editingElement: null,
|
editingElement: null,
|
||||||
activeTool: updateActiveTool(this.state, { type: "selection" }),
|
activeTool: updateActiveTool(this.state, { type: "selection" }),
|
||||||
},
|
},
|
||||||
|
@ -5569,7 +5626,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
} else {
|
} else {
|
||||||
ContextMenu.push({
|
ContextMenu.push({
|
||||||
options: [
|
options: [
|
||||||
this.deviceType.isMobile &&
|
this.device.isMobile &&
|
||||||
navigator.clipboard && {
|
navigator.clipboard && {
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
name: "paste",
|
name: "paste",
|
||||||
|
@ -5581,7 +5638,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.paste",
|
contextItemLabel: "labels.paste",
|
||||||
},
|
},
|
||||||
this.deviceType.isMobile && navigator.clipboard && separator,
|
this.device.isMobile && navigator.clipboard && separator,
|
||||||
probablySupportsClipboardBlob &&
|
probablySupportsClipboardBlob &&
|
||||||
elements.length > 0 &&
|
elements.length > 0 &&
|
||||||
actionCopyAsPng,
|
actionCopyAsPng,
|
||||||
|
@ -5626,9 +5683,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
} else {
|
} else {
|
||||||
ContextMenu.push({
|
ContextMenu.push({
|
||||||
options: [
|
options: [
|
||||||
this.deviceType.isMobile && actionCut,
|
this.device.isMobile && actionCut,
|
||||||
this.deviceType.isMobile && navigator.clipboard && actionCopy,
|
this.device.isMobile && navigator.clipboard && actionCopy,
|
||||||
this.deviceType.isMobile &&
|
this.device.isMobile &&
|
||||||
navigator.clipboard && {
|
navigator.clipboard && {
|
||||||
name: "paste",
|
name: "paste",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
|
@ -5640,7 +5697,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.paste",
|
contextItemLabel: "labels.paste",
|
||||||
},
|
},
|
||||||
this.deviceType.isMobile && separator,
|
this.device.isMobile && separator,
|
||||||
...options,
|
...options,
|
||||||
separator,
|
separator,
|
||||||
actionCopyStyles,
|
actionCopyStyles,
|
||||||
|
@ -5891,10 +5948,10 @@ if (
|
||||||
elements: {
|
elements: {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
get() {
|
get() {
|
||||||
return this.app.scene.getElementsIncludingDeleted();
|
return this.app?.scene.getElementsIncludingDeleted();
|
||||||
},
|
},
|
||||||
set(elements: ExcalidrawElement[]) {
|
set(elements: ExcalidrawElement[]) {
|
||||||
return this.app.scene.replaceAllElements(elements);
|
return this.app?.scene.replaceAllElements(elements);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useDeviceType } from "./App";
|
import { useDevice } from "./App";
|
||||||
import { trash } from "./icons";
|
import { trash } from "./icons";
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ const ClearCanvas = ({ onConfirm }: { onConfirm: () => void }) => {
|
||||||
icon={trash}
|
icon={trash}
|
||||||
title={t("buttons.clearReset")}
|
title={t("buttons.clearReset")}
|
||||||
aria-label={t("buttons.clearReset")}
|
aria-label={t("buttons.clearReset")}
|
||||||
showAriaLabel={useDeviceType().isMobile}
|
showAriaLabel={useDevice().isMobile}
|
||||||
onClick={toggleDialog}
|
onClick={toggleDialog}
|
||||||
data-testid="clear-canvas-button"
|
data-testid="clear-canvas-button"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -18,13 +18,15 @@
|
||||||
left: -5px;
|
left: -5px;
|
||||||
}
|
}
|
||||||
min-width: 1em;
|
min-width: 1em;
|
||||||
|
min-height: 1em;
|
||||||
|
line-height: 1;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -5px;
|
bottom: -5px;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: $oc-green-6;
|
background-color: $oc-green-6;
|
||||||
color: $oc-white;
|
color: $oc-white;
|
||||||
font-size: 0.7em;
|
font-size: 0.6em;
|
||||||
font-family: var(--ui-font);
|
font-family: "Cascadia";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useDeviceType } from "../components/App";
|
import { useDevice } from "../components/App";
|
||||||
import { users } from "./icons";
|
import { users } from "./icons";
|
||||||
|
|
||||||
import "./CollabButton.scss";
|
import "./CollabButton.scss";
|
||||||
|
@ -26,9 +26,9 @@ const CollabButton = ({
|
||||||
type="button"
|
type="button"
|
||||||
title={t("labels.liveCollaboration")}
|
title={t("labels.liveCollaboration")}
|
||||||
aria-label={t("labels.liveCollaboration")}
|
aria-label={t("labels.liveCollaboration")}
|
||||||
showAriaLabel={useDeviceType().isMobile}
|
showAriaLabel={useDevice().isMobile}
|
||||||
>
|
>
|
||||||
{collaboratorCount > 0 && (
|
{isCollaborating && (
|
||||||
<div className="CollabButton-collaborators">{collaboratorCount}</div>
|
<div className="CollabButton-collaborators">{collaboratorCount}</div>
|
||||||
)}
|
)}
|
||||||
</ToolButton>
|
</ToolButton>
|
||||||
|
|
|
@ -128,31 +128,21 @@ const Picker = ({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||||
if (event.key === KEYS.TAB) {
|
let handled = false;
|
||||||
const { activeElement } = document;
|
if (isArrowKey(event.key)) {
|
||||||
if (event.shiftKey) {
|
handled = true;
|
||||||
if (activeElement === firstItem.current) {
|
|
||||||
colorInput.current?.focus();
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
} else if (activeElement === colorInput.current) {
|
|
||||||
firstItem.current?.focus();
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
} else if (isArrowKey(event.key)) {
|
|
||||||
const { activeElement } = document;
|
const { activeElement } = document;
|
||||||
const isRTL = getLanguage().rtl;
|
const isRTL = getLanguage().rtl;
|
||||||
let isCustom = false;
|
let isCustom = false;
|
||||||
let index = Array.prototype.indexOf.call(
|
let index = Array.prototype.indexOf.call(
|
||||||
gallery.current!.querySelector(".color-picker-content--default")!
|
gallery.current!.querySelector(".color-picker-content--default")
|
||||||
.children,
|
?.children,
|
||||||
activeElement,
|
activeElement,
|
||||||
);
|
);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
index = Array.prototype.indexOf.call(
|
index = Array.prototype.indexOf.call(
|
||||||
gallery.current!.querySelector(
|
gallery.current!.querySelector(".color-picker-content--canvas-colors")
|
||||||
".color-picker-content--canvas-colors",
|
?.children,
|
||||||
)!.children,
|
|
||||||
activeElement,
|
activeElement,
|
||||||
);
|
);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
|
@ -180,8 +170,11 @@ const Picker = ({
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else if (
|
} else if (
|
||||||
keyBindings.includes(event.key.toLowerCase()) &&
|
keyBindings.includes(event.key.toLowerCase()) &&
|
||||||
|
!event[KEYS.CTRL_OR_CMD] &&
|
||||||
|
!event.altKey &&
|
||||||
!isWritableElement(event.target)
|
!isWritableElement(event.target)
|
||||||
) {
|
) {
|
||||||
|
handled = true;
|
||||||
const index = keyBindings.indexOf(event.key.toLowerCase());
|
const index = keyBindings.indexOf(event.key.toLowerCase());
|
||||||
const isCustom = index >= MAX_DEFAULT_COLORS;
|
const isCustom = index >= MAX_DEFAULT_COLORS;
|
||||||
const parentElement = isCustom
|
const parentElement = isCustom
|
||||||
|
@ -196,11 +189,14 @@ const Picker = ({
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) {
|
} else if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) {
|
||||||
|
handled = true;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
event.nativeEvent.stopImmediatePropagation();
|
if (handled) {
|
||||||
event.stopPropagation();
|
event.nativeEvent.stopImmediatePropagation();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderColors = (colors: Array<string>, custom: boolean = false) => {
|
const renderColors = (colors: Array<string>, custom: boolean = false) => {
|
||||||
|
@ -264,7 +260,8 @@ const Picker = ({
|
||||||
gallery.current = el;
|
gallery.current = el;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
tabIndex={0}
|
// to allow focusing by clicking but not by tabbing
|
||||||
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<div className="color-picker-content--default">
|
<div className="color-picker-content--default">
|
||||||
{renderColors(colors)}
|
{renderColors(colors)}
|
||||||
|
|
|
@ -2,13 +2,14 @@ import clsx from "clsx";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useCallbackRefState } from "../hooks/useCallbackRefState";
|
import { useCallbackRefState } from "../hooks/useCallbackRefState";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useExcalidrawContainer, useDeviceType } from "../components/App";
|
import { useExcalidrawContainer, useDevice } from "../components/App";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import "./Dialog.scss";
|
import "./Dialog.scss";
|
||||||
import { back, close } from "./icons";
|
import { back, close } from "./icons";
|
||||||
import { Island } from "./Island";
|
import { Island } from "./Island";
|
||||||
import { Modal } from "./Modal";
|
import { Modal } from "./Modal";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
|
import { queryFocusableElements } from "../utils";
|
||||||
|
|
||||||
export interface DialogProps {
|
export interface DialogProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
@ -64,14 +65,6 @@ export const Dialog = (props: DialogProps) => {
|
||||||
return () => islandNode.removeEventListener("keydown", handleKeyDown);
|
return () => islandNode.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [islandNode, props.autofocus]);
|
}, [islandNode, props.autofocus]);
|
||||||
|
|
||||||
const queryFocusableElements = (node: HTMLElement) => {
|
|
||||||
const focusableElements = node.querySelectorAll<HTMLElement>(
|
|
||||||
"button, a, input, select, textarea, div[tabindex]",
|
|
||||||
);
|
|
||||||
|
|
||||||
return focusableElements ? Array.from(focusableElements) : [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
(lastActiveElement as HTMLElement).focus();
|
(lastActiveElement as HTMLElement).focus();
|
||||||
props.onCloseRequest();
|
props.onCloseRequest();
|
||||||
|
@ -94,7 +87,7 @@ export const Dialog = (props: DialogProps) => {
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
aria-label={t("buttons.close")}
|
aria-label={t("buttons.close")}
|
||||||
>
|
>
|
||||||
{useDeviceType().isMobile ? back : close}
|
{useDevice().isMobile ? back : close}
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div className="Dialog__content">{props.children}</div>
|
<div className="Dialog__content">{props.children}</div>
|
||||||
|
|
|
@ -45,7 +45,7 @@ const getHints = ({ appState, elements, isMobile }: HintViewerProps) => {
|
||||||
return t("hints.text");
|
return t("hints.text");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appState.activeTool.type === "image" && appState.pendingImageElement) {
|
if (appState.activeTool.type === "image" && appState.pendingImageElementId) {
|
||||||
return t("hints.placeImage");
|
return t("hints.placeImage");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { canvasToBlob } from "../data/blob";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { CanvasError } from "../errors";
|
import { CanvasError } from "../errors";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useDeviceType } from "./App";
|
import { useDevice } from "./App";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
import { exportToCanvas } from "../scene/export";
|
import { exportToCanvas } from "../scene/export";
|
||||||
import { AppState, BinaryFiles } from "../types";
|
import { AppState, BinaryFiles } from "../types";
|
||||||
|
@ -250,7 +250,7 @@ export const ImageExportDialog = ({
|
||||||
icon={exportImage}
|
icon={exportImage}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={t("buttons.exportImage")}
|
aria-label={t("buttons.exportImage")}
|
||||||
showAriaLabel={useDeviceType().isMobile}
|
showAriaLabel={useDevice().isMobile}
|
||||||
title={t("buttons.exportImage")}
|
title={t("buttons.exportImage")}
|
||||||
/>
|
/>
|
||||||
{modalIsShown && (
|
{modalIsShown && (
|
||||||
|
|
|
@ -14,11 +14,11 @@ export const InitializeApp = (props: Props) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateLang = async () => {
|
const updateLang = async () => {
|
||||||
await setLanguage(currentLang);
|
await setLanguage(currentLang);
|
||||||
|
setLoading(false);
|
||||||
};
|
};
|
||||||
const currentLang =
|
const currentLang =
|
||||||
languages.find((lang) => lang.code === props.langCode) || defaultLang;
|
languages.find((lang) => lang.code === props.langCode) || defaultLang;
|
||||||
updateLang();
|
updateLang();
|
||||||
setLoading(false);
|
|
||||||
}, [props.langCode]);
|
}, [props.langCode]);
|
||||||
|
|
||||||
return loading ? <LoadingMessage /> : props.children;
|
return loading ? <LoadingMessage /> : props.children;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useDeviceType } from "./App";
|
import { useDevice } from "./App";
|
||||||
import { AppState, ExportOpts, BinaryFiles } from "../types";
|
import { AppState, ExportOpts, BinaryFiles } from "../types";
|
||||||
import { Dialog } from "./Dialog";
|
import { Dialog } from "./Dialog";
|
||||||
import { exportFile, exportToFileIcon, link } from "./icons";
|
import { exportFile, exportToFileIcon, link } from "./icons";
|
||||||
|
@ -117,7 +117,7 @@ export const JSONExportDialog = ({
|
||||||
icon={exportFile}
|
icon={exportFile}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={t("buttons.export")}
|
aria-label={t("buttons.export")}
|
||||||
showAriaLabel={useDeviceType().isMobile}
|
showAriaLabel={useDevice().isMobile}
|
||||||
title={t("buttons.export")}
|
title={t("buttons.export")}
|
||||||
/>
|
/>
|
||||||
{modalIsShown && (
|
{modalIsShown && (
|
||||||
|
|
|
@ -1,9 +1,63 @@
|
||||||
@import "open-color/open-color";
|
@import "open-color/open-color";
|
||||||
|
@import "../css/variables.module";
|
||||||
|
|
||||||
|
.layer-ui__sidebar {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--sat);
|
||||||
|
bottom: var(--sab);
|
||||||
|
right: var(--sar);
|
||||||
|
z-index: 5;
|
||||||
|
|
||||||
|
box-shadow: var(--shadow-island);
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
margin: var(--space-factor);
|
||||||
|
width: calc(#{$right-sidebar-width} - var(--space-factor) * 2);
|
||||||
|
|
||||||
|
.Island {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToolIcon__icon {
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToolIcon__icon__close {
|
||||||
|
.Modal__close {
|
||||||
|
width: calc(var(--space-factor) * 7);
|
||||||
|
height: calc(var(--space-factor) * 7);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Island {
|
||||||
|
--padding: 0;
|
||||||
|
background-color: var(--island-bg-color);
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
padding: calc(var(--padding) * var(--space-factor));
|
||||||
|
position: relative;
|
||||||
|
transition: box-shadow 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
|
.layer-ui__wrapper.animate {
|
||||||
|
transition: width 0.1s ease-in-out;
|
||||||
|
}
|
||||||
.layer-ui__wrapper {
|
.layer-ui__wrapper {
|
||||||
|
// when the rightside sidebar is docked, we need to resize the UI by its
|
||||||
|
// width, making the nested UI content shift to the left. To do this,
|
||||||
|
// we need the UI container to actually have dimensions set, but
|
||||||
|
// then we also need to disable pointer events else the canvas below
|
||||||
|
// wouldn't be interactive.
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
z-index: var(--zIndex-layerUI);
|
z-index: var(--zIndex-layerUI);
|
||||||
|
|
||||||
&__top-right {
|
&__top-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import React, { useCallback } from "react";
|
import React, { useCallback } from "react";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { CLASSES } from "../constants";
|
import { CLASSES, LIBRARY_SIDEBAR_WIDTH } from "../constants";
|
||||||
import { exportCanvas } from "../data";
|
import { exportCanvas } from "../data";
|
||||||
import { isTextElement, showSelectedShapeActions } from "../element";
|
import { isTextElement, showSelectedShapeActions } from "../element";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
|
@ -25,7 +25,6 @@ import { PasteChartDialog } from "./PasteChartDialog";
|
||||||
import { Section } from "./Section";
|
import { Section } from "./Section";
|
||||||
import { HelpDialog } from "./HelpDialog";
|
import { HelpDialog } from "./HelpDialog";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import { Tooltip } from "./Tooltip";
|
|
||||||
import { UserList } from "./UserList";
|
import { UserList } from "./UserList";
|
||||||
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
|
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
|
||||||
import { JSONExportDialog } from "./JSONExportDialog";
|
import { JSONExportDialog } from "./JSONExportDialog";
|
||||||
|
@ -37,7 +36,9 @@ import "./LayerUI.scss";
|
||||||
import "./Toolbar.scss";
|
import "./Toolbar.scss";
|
||||||
import { PenModeButton } from "./PenModeButton";
|
import { PenModeButton } from "./PenModeButton";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { useDeviceType } from "../components/App";
|
import { useDevice } from "../components/App";
|
||||||
|
import { Stats } from "./Stats";
|
||||||
|
import { actionToggleStats } from "../actions/actionToggleStats";
|
||||||
|
|
||||||
interface LayerUIProps {
|
interface LayerUIProps {
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
|
@ -56,14 +57,9 @@ interface LayerUIProps {
|
||||||
toggleZenMode: () => void;
|
toggleZenMode: () => void;
|
||||||
langCode: Language["code"];
|
langCode: Language["code"];
|
||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
renderTopRightUI?: (
|
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
|
||||||
isMobile: boolean,
|
renderCustomFooter?: ExcalidrawProps["renderFooter"];
|
||||||
appState: AppState,
|
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||||
) => JSX.Element | null;
|
|
||||||
renderCustomFooter?: (
|
|
||||||
isMobile: boolean,
|
|
||||||
appState: AppState,
|
|
||||||
) => JSX.Element | null;
|
|
||||||
viewModeEnabled: boolean;
|
viewModeEnabled: boolean;
|
||||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||||
UIOptions: AppProps["UIOptions"];
|
UIOptions: AppProps["UIOptions"];
|
||||||
|
@ -72,7 +68,6 @@ interface LayerUIProps {
|
||||||
id: string;
|
id: string;
|
||||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LayerUI = ({
|
const LayerUI = ({
|
||||||
actionManager,
|
actionManager,
|
||||||
appState,
|
appState,
|
||||||
|
@ -91,6 +86,7 @@ const LayerUI = ({
|
||||||
isCollaborating,
|
isCollaborating,
|
||||||
renderTopRightUI,
|
renderTopRightUI,
|
||||||
renderCustomFooter,
|
renderCustomFooter,
|
||||||
|
renderCustomStats,
|
||||||
viewModeEnabled,
|
viewModeEnabled,
|
||||||
libraryReturnUrl,
|
libraryReturnUrl,
|
||||||
UIOptions,
|
UIOptions,
|
||||||
|
@ -99,7 +95,7 @@ const LayerUI = ({
|
||||||
id,
|
id,
|
||||||
onImageAction,
|
onImageAction,
|
||||||
}: LayerUIProps) => {
|
}: LayerUIProps) => {
|
||||||
const deviceType = useDeviceType();
|
const device = useDevice();
|
||||||
|
|
||||||
const renderJSONExportDialog = () => {
|
const renderJSONExportDialog = () => {
|
||||||
if (!UIOptions.canvasActions.export) {
|
if (!UIOptions.canvasActions.export) {
|
||||||
|
@ -345,7 +341,7 @@ const LayerUI = ({
|
||||||
<HintViewer
|
<HintViewer
|
||||||
appState={appState}
|
appState={appState}
|
||||||
elements={elements}
|
elements={elements}
|
||||||
isMobile={deviceType.isMobile}
|
isMobile={device.isMobile}
|
||||||
/>
|
/>
|
||||||
{heading}
|
{heading}
|
||||||
<Stack.Row gap={1}>
|
<Stack.Row gap={1}>
|
||||||
|
@ -367,7 +363,6 @@ const LayerUI = ({
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
/>
|
/>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
{libraryMenu}
|
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
)}
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
|
@ -380,23 +375,11 @@ const LayerUI = ({
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<UserList>
|
<UserList
|
||||||
{appState.collaborators.size > 0 &&
|
collaborators={appState.collaborators}
|
||||||
Array.from(appState.collaborators)
|
actionManager={actionManager}
|
||||||
// Collaborator is either not initialized or is actually the current user.
|
/>
|
||||||
.filter(([_, client]) => Object.keys(client).length !== 0)
|
{renderTopRightUI?.(device.isMobile, appState)}
|
||||||
.map(([clientId, client]) => (
|
|
||||||
<Tooltip
|
|
||||||
label={client.username || "Unknown user"}
|
|
||||||
key={clientId}
|
|
||||||
>
|
|
||||||
{actionManager.renderAction("goToCollaborator", {
|
|
||||||
id: clientId,
|
|
||||||
})}
|
|
||||||
</Tooltip>
|
|
||||||
))}
|
|
||||||
</UserList>
|
|
||||||
{renderTopRightUI?.(deviceType.isMobile, appState)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FixedSideContainer>
|
</FixedSideContainer>
|
||||||
|
@ -449,7 +432,7 @@ const LayerUI = ({
|
||||||
)}
|
)}
|
||||||
{!viewModeEnabled &&
|
{!viewModeEnabled &&
|
||||||
appState.multiElement &&
|
appState.multiElement &&
|
||||||
deviceType.isTouchScreen && (
|
device.isTouchScreen && (
|
||||||
<div
|
<div
|
||||||
className={clsx("finalize-button zen-mode-transition", {
|
className={clsx("finalize-button zen-mode-transition", {
|
||||||
"layer-ui__wrapper__footer-left--transition-left":
|
"layer-ui__wrapper__footer-left--transition-left":
|
||||||
|
@ -526,7 +509,24 @@ const LayerUI = ({
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
return deviceType.isMobile ? (
|
const renderStats = () => {
|
||||||
|
if (!appState.showStats) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Stats
|
||||||
|
appState={appState}
|
||||||
|
setAppState={setAppState}
|
||||||
|
elements={elements}
|
||||||
|
onClose={() => {
|
||||||
|
actionManager.executeAction(actionToggleStats);
|
||||||
|
}}
|
||||||
|
renderCustomStats={renderCustomStats}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return device.isMobile ? (
|
||||||
<>
|
<>
|
||||||
{dialogs}
|
{dialogs}
|
||||||
<MobileMenu
|
<MobileMenu
|
||||||
|
@ -547,33 +547,48 @@ const LayerUI = ({
|
||||||
showThemeBtn={showThemeBtn}
|
showThemeBtn={showThemeBtn}
|
||||||
onImageAction={onImageAction}
|
onImageAction={onImageAction}
|
||||||
renderTopRightUI={renderTopRightUI}
|
renderTopRightUI={renderTopRightUI}
|
||||||
|
renderStats={renderStats}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<>
|
||||||
className={clsx("layer-ui__wrapper", {
|
<div
|
||||||
"disable-pointerEvents":
|
className={clsx("layer-ui__wrapper", {
|
||||||
appState.draggingElement ||
|
"disable-pointerEvents":
|
||||||
appState.resizingElement ||
|
appState.draggingElement ||
|
||||||
(appState.editingElement && !isTextElement(appState.editingElement)),
|
appState.resizingElement ||
|
||||||
})}
|
(appState.editingElement &&
|
||||||
>
|
!isTextElement(appState.editingElement)),
|
||||||
{dialogs}
|
})}
|
||||||
{renderFixedSideContainer()}
|
style={
|
||||||
{renderBottomAppMenu()}
|
appState.isLibraryOpen &&
|
||||||
{appState.scrolledOutside && (
|
appState.isLibraryMenuDocked &&
|
||||||
<button
|
device.canDeviceFitSidebar
|
||||||
className="scroll-back-to-content"
|
? { width: `calc(100% - ${LIBRARY_SIDEBAR_WIDTH}px)` }
|
||||||
onClick={() => {
|
: {}
|
||||||
setAppState({
|
}
|
||||||
...calculateScrollCenter(elements, appState, canvas),
|
>
|
||||||
});
|
{dialogs}
|
||||||
}}
|
{renderFixedSideContainer()}
|
||||||
>
|
{renderBottomAppMenu()}
|
||||||
{t("buttons.scrollBackToContent")}
|
{renderStats()}
|
||||||
</button>
|
{appState.scrolledOutside && (
|
||||||
|
<button
|
||||||
|
className="scroll-back-to-content"
|
||||||
|
onClick={() => {
|
||||||
|
setAppState({
|
||||||
|
...calculateScrollCenter(elements, appState, canvas),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("buttons.scrollBackToContent")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{appState.isLibraryOpen && (
|
||||||
|
<div className="layer-ui__sidebar">{libraryMenu}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@ import clsx from "clsx";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { capitalizeString } from "../utils";
|
import { capitalizeString } from "../utils";
|
||||||
|
import { trackEvent } from "../analytics";
|
||||||
|
import { useDevice } from "./App";
|
||||||
|
|
||||||
const LIBRARY_ICON = (
|
const LIBRARY_ICON = (
|
||||||
<svg viewBox="0 0 576 512">
|
<svg viewBox="0 0 576 512">
|
||||||
|
@ -18,6 +20,7 @@ export const LibraryButton: React.FC<{
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
isMobile?: boolean;
|
isMobile?: boolean;
|
||||||
}> = ({ appState, setAppState, isMobile }) => {
|
}> = ({ appState, setAppState, isMobile }) => {
|
||||||
|
const device = useDevice();
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
@ -34,7 +37,19 @@ export const LibraryButton: React.FC<{
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="editor-library"
|
name="editor-library"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setAppState({ isLibraryOpen: event.target.checked });
|
document
|
||||||
|
.querySelector(".layer-ui__wrapper")
|
||||||
|
?.classList.remove("animate");
|
||||||
|
const nextState = event.target.checked;
|
||||||
|
setAppState({ isLibraryOpen: nextState });
|
||||||
|
// track only openings
|
||||||
|
if (nextState) {
|
||||||
|
trackEvent(
|
||||||
|
"library",
|
||||||
|
"toggleLibrary (open)",
|
||||||
|
`toolbar (${device.isMobile ? "mobile" : "desktop"})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
checked={appState.isLibraryOpen}
|
checked={appState.isLibraryOpen}
|
||||||
aria-label={capitalizeString(t("toolBar.library"))}
|
aria-label={capitalizeString(t("toolBar.library"))}
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.layer-ui__library {
|
.layer-ui__library {
|
||||||
margin: auto;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -11,8 +10,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 2px 0;
|
margin: 2px 0 15px 0;
|
||||||
|
|
||||||
.Spinner {
|
.Spinner {
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
}
|
}
|
||||||
|
@ -21,13 +19,17 @@
|
||||||
// 2px from the left to account for focus border of left-most button
|
// 2px from the left to account for focus border of left-most button
|
||||||
margin: 0 2px;
|
margin: 0 2px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
.layer-ui__sidebar {
|
||||||
margin-inline-start: auto;
|
.layer-ui__library {
|
||||||
// 17px for scrollbar (needed for overlay scrollbars on Big Sur?) + 1px extra
|
padding: 0;
|
||||||
padding-inline-end: 18px;
|
height: 100%;
|
||||||
white-space: nowrap;
|
}
|
||||||
}
|
.library-menu-items-container {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,4 +67,38 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.library-menu-browse-button {
|
||||||
|
width: 80%;
|
||||||
|
min-height: 22px;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
color: $oc-white;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-decoration: none !important;
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-primary-darker);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: var(--color-primary-darkest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-menu-browse-button--mobile {
|
||||||
|
min-height: 22px;
|
||||||
|
margin-left: auto;
|
||||||
|
a {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { trackEvent } from "../analytics";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { jotaiScope } from "../jotai";
|
import { jotaiScope } from "../jotai";
|
||||||
import Spinner from "./Spinner";
|
import Spinner from "./Spinner";
|
||||||
|
import { useDevice } from "./App";
|
||||||
|
|
||||||
const useOnClickOutside = (
|
const useOnClickOutside = (
|
||||||
ref: RefObject<HTMLElement>,
|
ref: RefObject<HTMLElement>,
|
||||||
|
@ -103,17 +104,30 @@ export const LibraryMenu = ({
|
||||||
}) => {
|
}) => {
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
useOnClickOutside(ref, (event) => {
|
const device = useDevice();
|
||||||
// If click on the library icon, do nothing.
|
|
||||||
if ((event.target as Element).closest(".ToolIcon__library")) {
|
useOnClickOutside(
|
||||||
return;
|
ref,
|
||||||
}
|
useCallback(
|
||||||
onClose();
|
(event) => {
|
||||||
});
|
// If click on the library icon, do nothing.
|
||||||
|
if ((event.target as Element).closest(".ToolIcon__library")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!appState.isLibraryMenuDocked || !device.canDeviceFitSidebar) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onClose, appState.isLibraryMenuDocked, device.canDeviceFitSidebar],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === KEYS.ESCAPE) {
|
if (
|
||||||
|
event.key === KEYS.ESCAPE &&
|
||||||
|
(!appState.isLibraryMenuDocked || !device.canDeviceFitSidebar)
|
||||||
|
) {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -121,7 +135,7 @@ export const LibraryMenu = ({
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
|
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
|
||||||
};
|
};
|
||||||
}, [onClose]);
|
}, [onClose, appState.isLibraryMenuDocked, device.canDeviceFitSidebar]);
|
||||||
|
|
||||||
const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]);
|
const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]);
|
||||||
const [showPublishLibraryDialog, setShowPublishLibraryDialog] =
|
const [showPublishLibraryDialog, setShowPublishLibraryDialog] =
|
||||||
|
@ -273,6 +287,7 @@ export const LibraryMenu = ({
|
||||||
onInsertLibraryItems={onInsertLibraryItems}
|
onInsertLibraryItems={onInsertLibraryItems}
|
||||||
pendingElements={pendingElements}
|
pendingElements={pendingElements}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
|
appState={appState}
|
||||||
libraryReturnUrl={libraryReturnUrl}
|
libraryReturnUrl={libraryReturnUrl}
|
||||||
library={library}
|
library={library}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
|
|
@ -2,8 +2,17 @@
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.library-menu-items-container {
|
.library-menu-items-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
.library-actions {
|
.library-actions {
|
||||||
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
margin-right: auto;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
button .library-actions-counter {
|
button .library-actions-counter {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -87,12 +96,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&__items {
|
&__items {
|
||||||
max-height: 50vh;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow-y: auto;
|
||||||
margin-top: 0.5rem;
|
overflow-x: hidden;
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.separator {
|
.separator {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
margin: 0.6em 0.2em;
|
margin: 0.6em 0.2em;
|
||||||
|
|
|
@ -12,9 +12,9 @@ import {
|
||||||
LibraryItems,
|
LibraryItems,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { arrayToMap, muteFSAbortError } from "../utils";
|
import { arrayToMap, muteFSAbortError } from "../utils";
|
||||||
import { useDeviceType } from "./App";
|
import { useDevice } from "./App";
|
||||||
import ConfirmDialog from "./ConfirmDialog";
|
import ConfirmDialog from "./ConfirmDialog";
|
||||||
import { exportToFileIcon, load, publishIcon, trash } from "./icons";
|
import { close, exportToFileIcon, load, publishIcon, trash } from "./icons";
|
||||||
import { LibraryUnit } from "./LibraryUnit";
|
import { LibraryUnit } from "./LibraryUnit";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
|
@ -25,6 +25,9 @@ import { MIME_TYPES, VERSIONS } from "../constants";
|
||||||
import Spinner from "./Spinner";
|
import Spinner from "./Spinner";
|
||||||
import { fileOpen } from "../data/filesystem";
|
import { fileOpen } from "../data/filesystem";
|
||||||
|
|
||||||
|
import { SidebarLockButton } from "./SidebarLockButton";
|
||||||
|
import { trackEvent } from "../analytics";
|
||||||
|
|
||||||
const LibraryMenuItems = ({
|
const LibraryMenuItems = ({
|
||||||
isLoading,
|
isLoading,
|
||||||
libraryItems,
|
libraryItems,
|
||||||
|
@ -34,6 +37,7 @@ const LibraryMenuItems = ({
|
||||||
pendingElements,
|
pendingElements,
|
||||||
theme,
|
theme,
|
||||||
setAppState,
|
setAppState,
|
||||||
|
appState,
|
||||||
libraryReturnUrl,
|
libraryReturnUrl,
|
||||||
library,
|
library,
|
||||||
files,
|
files,
|
||||||
|
@ -52,6 +56,7 @@ const LibraryMenuItems = ({
|
||||||
theme: AppState["theme"];
|
theme: AppState["theme"];
|
||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
|
appState: AppState;
|
||||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||||
library: Library;
|
library: Library;
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -88,9 +93,7 @@ const LibraryMenuItems = ({
|
||||||
}, [selectedItems, onRemoveFromLibrary, resetLibrary]);
|
}, [selectedItems, onRemoveFromLibrary, resetLibrary]);
|
||||||
|
|
||||||
const [showRemoveLibAlert, setShowRemoveLibAlert] = useState(false);
|
const [showRemoveLibAlert, setShowRemoveLibAlert] = useState(false);
|
||||||
|
const device = useDevice();
|
||||||
const isMobile = useDeviceType().isMobile;
|
|
||||||
|
|
||||||
const renderLibraryActions = () => {
|
const renderLibraryActions = () => {
|
||||||
const itemsSelected = !!selectedItems.length;
|
const itemsSelected = !!selectedItems.length;
|
||||||
const items = itemsSelected
|
const items = itemsSelected
|
||||||
|
@ -101,7 +104,7 @@ const LibraryMenuItems = ({
|
||||||
: t("buttons.resetLibrary");
|
: t("buttons.resetLibrary");
|
||||||
return (
|
return (
|
||||||
<div className="library-actions">
|
<div className="library-actions">
|
||||||
{(!itemsSelected || !isMobile) && (
|
{!itemsSelected && (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
key="import"
|
key="import"
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -186,7 +189,7 @@ const LibraryMenuItems = ({
|
||||||
className="library-actions--publish"
|
className="library-actions--publish"
|
||||||
onClick={onPublish}
|
onClick={onPublish}
|
||||||
>
|
>
|
||||||
{!isMobile && <label>{t("buttons.publishLibrary")}</label>}
|
{!device.isMobile && <label>{t("buttons.publishLibrary")}</label>}
|
||||||
{selectedItems.length > 0 && (
|
{selectedItems.length > 0 && (
|
||||||
<span className="library-actions-counter">
|
<span className="library-actions-counter">
|
||||||
{selectedItems.length}
|
{selectedItems.length}
|
||||||
|
@ -195,11 +198,25 @@ const LibraryMenuItems = ({
|
||||||
</ToolButton>
|
</ToolButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
{device.isMobile && (
|
||||||
|
<div className="library-menu-browse-button--mobile">
|
||||||
|
<a
|
||||||
|
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
|
||||||
|
window.name || "_blank"
|
||||||
|
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
|
||||||
|
VERSIONS.excalidrawLibrary
|
||||||
|
}`}
|
||||||
|
target="_excalidraw_libraries"
|
||||||
|
>
|
||||||
|
{t("labels.libraries")}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const CELLS_PER_ROW = isMobile ? 4 : 6;
|
const CELLS_PER_ROW = device.isMobile && !device.isSmScreen ? 6 : 4;
|
||||||
|
|
||||||
const referrer =
|
const referrer =
|
||||||
libraryReturnUrl || window.location.origin + window.location.pathname;
|
libraryReturnUrl || window.location.origin + window.location.pathname;
|
||||||
|
@ -356,48 +373,185 @@ const LibraryMenuItems = ({
|
||||||
(item) => item.status === "published",
|
(item) => item.status === "published",
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
const renderLibraryHeader = () => {
|
||||||
<div className="library-menu-items-container">
|
return (
|
||||||
{showRemoveLibAlert && renderRemoveLibAlert()}
|
<>
|
||||||
<div className="layer-ui__library-header" key="library-header">
|
<div className="layer-ui__library-header" key="library-header">
|
||||||
{renderLibraryActions()}
|
{renderLibraryActions()}
|
||||||
{isLoading ? (
|
{device.canDeviceFitSidebar && (
|
||||||
<Spinner />
|
<>
|
||||||
) : (
|
<div className="layer-ui__sidebar-lock-button">
|
||||||
<a
|
<SidebarLockButton
|
||||||
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
|
checked={appState.isLibraryMenuDocked}
|
||||||
window.name || "_blank"
|
onChange={() => {
|
||||||
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
|
document
|
||||||
VERSIONS.excalidrawLibrary
|
.querySelector(".layer-ui__wrapper")
|
||||||
}`}
|
?.classList.add("animate");
|
||||||
target="_excalidraw_libraries"
|
const nextState = !appState.isLibraryMenuDocked;
|
||||||
>
|
setAppState({
|
||||||
{t("labels.libraries")}
|
isLibraryMenuDocked: nextState,
|
||||||
</a>
|
});
|
||||||
)}
|
trackEvent(
|
||||||
</div>
|
"library",
|
||||||
|
`toggleLibraryDock (${nextState ? "dock" : "undock"})`,
|
||||||
|
`sidebar (${device.isMobile ? "mobile" : "desktop"})`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!device.isMobile && (
|
||||||
|
<div className="ToolIcon__icon__close">
|
||||||
|
<button
|
||||||
|
className="Modal__close"
|
||||||
|
onClick={() =>
|
||||||
|
setAppState({
|
||||||
|
isLibraryOpen: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
aria-label={t("buttons.close")}
|
||||||
|
>
|
||||||
|
{close}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderLibraryMenuItems = () => {
|
||||||
|
return (
|
||||||
<Stack.Col
|
<Stack.Col
|
||||||
className="library-menu-items-container__items"
|
className="library-menu-items-container__items"
|
||||||
align="start"
|
align="start"
|
||||||
gap={1}
|
gap={1}
|
||||||
|
style={{
|
||||||
|
flex: publishedItems.length > 0 ? 1 : "0 1 auto",
|
||||||
|
marginBottom: 0,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<div className="separator">{t("labels.personalLib")}</div>
|
<div className="separator">
|
||||||
{renderLibrarySection([
|
{(pendingElements.length > 0 ||
|
||||||
// append pending library item
|
unpublishedItems.length > 0 ||
|
||||||
...(pendingElements.length
|
publishedItems.length > 0) && (
|
||||||
? [{ id: null, elements: pendingElements }]
|
<div>{t("labels.personalLib")}</div>
|
||||||
: []),
|
)}
|
||||||
...unpublishedItems,
|
{isLoading && (
|
||||||
])}
|
<div
|
||||||
|
style={{
|
||||||
|
marginLeft: "auto",
|
||||||
|
marginRight: "1rem",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
fontWeight: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ transform: "translateY(2px)" }}>
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!pendingElements.length && !unpublishedItems.length ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: 65,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "100%",
|
||||||
|
fontSize: ".9rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("library.noItems")}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: ".6rem 0",
|
||||||
|
fontSize: ".8em",
|
||||||
|
width: "70%",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{publishedItems.length > 0
|
||||||
|
? t("library.hint_emptyPrivateLibrary")
|
||||||
|
: t("library.hint_emptyLibrary")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
renderLibrarySection([
|
||||||
|
// append pending library item
|
||||||
|
...(pendingElements.length
|
||||||
|
? [{ id: null, elements: pendingElements }]
|
||||||
|
: []),
|
||||||
|
...unpublishedItems,
|
||||||
|
])
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
||||||
<>
|
<>
|
||||||
<div className="separator">{t("labels.excalidrawLib")} </div>
|
{(publishedItems.length > 0 ||
|
||||||
|
(!device.isMobile &&
|
||||||
{renderLibrarySection(publishedItems)}
|
(pendingElements.length > 0 || unpublishedItems.length > 0))) && (
|
||||||
|
<div className="separator">{t("labels.excalidrawLib")}</div>
|
||||||
|
)}
|
||||||
|
{publishedItems.length > 0 ? (
|
||||||
|
renderLibrarySection(publishedItems)
|
||||||
|
) : unpublishedItems.length > 0 ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "1rem 0",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "100%",
|
||||||
|
fontSize: ".9rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("library.noItems")}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderLibraryFooter = () => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className="library-menu-browse-button"
|
||||||
|
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
|
||||||
|
window.name || "_blank"
|
||||||
|
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
|
||||||
|
VERSIONS.excalidrawLibrary
|
||||||
|
}`}
|
||||||
|
target="_excalidraw_libraries"
|
||||||
|
>
|
||||||
|
{t("labels.libraries")}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="library-menu-items-container"
|
||||||
|
style={
|
||||||
|
device.isMobile
|
||||||
|
? {
|
||||||
|
minHeight: "200px",
|
||||||
|
maxHeight: "70vh",
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{showRemoveLibAlert && renderRemoveLibAlert()}
|
||||||
|
{renderLibraryHeader()}
|
||||||
|
{renderLibraryMenuItems()}
|
||||||
|
{!device.isMobile && renderLibraryFooter()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.library-unit {
|
.library-unit {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: 1px solid var(--button-gray-2);
|
border: 1px solid transparent;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -21,10 +21,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.theme--dark .library-unit {
|
|
||||||
border-color: rgb(48, 48, 48);
|
|
||||||
}
|
|
||||||
|
|
||||||
.library-unit__dragger {
|
.library-unit__dragger {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useDeviceType } from "../components/App";
|
import { useDevice } from "../components/App";
|
||||||
import { exportToSvg } from "../scene/export";
|
import { exportToSvg } from "../scene/export";
|
||||||
import { BinaryFiles, LibraryItem } from "../types";
|
import { BinaryFiles, LibraryItem } from "../types";
|
||||||
import "./LibraryUnit.scss";
|
import "./LibraryUnit.scss";
|
||||||
|
@ -67,7 +67,7 @@ export const LibraryUnit = ({
|
||||||
}, [elements, files]);
|
}, [elements, files]);
|
||||||
|
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const isMobile = useDeviceType().isMobile;
|
const isMobile = useDevice().isMobile;
|
||||||
const adder = isPending && (
|
const adder = isPending && (
|
||||||
<div className="library-unit__adder">{PLUS_ICON}</div>
|
<div className="library-unit__adder">{PLUS_ICON}</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -43,6 +43,7 @@ type MobileMenuProps = {
|
||||||
isMobile: boolean,
|
isMobile: boolean,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => JSX.Element | null;
|
) => JSX.Element | null;
|
||||||
|
renderStats: () => JSX.Element | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MobileMenu = ({
|
export const MobileMenu = ({
|
||||||
|
@ -63,6 +64,7 @@ export const MobileMenu = ({
|
||||||
showThemeBtn,
|
showThemeBtn,
|
||||||
onImageAction,
|
onImageAction,
|
||||||
renderTopRightUI,
|
renderTopRightUI,
|
||||||
|
renderStats,
|
||||||
}: MobileMenuProps) => {
|
}: MobileMenuProps) => {
|
||||||
const renderToolbar = () => {
|
const renderToolbar = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -184,6 +186,7 @@ export const MobileMenu = ({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!viewModeEnabled && renderToolbar()}
|
{!viewModeEnabled && renderToolbar()}
|
||||||
|
{renderStats()}
|
||||||
<div
|
<div
|
||||||
className="App-bottom-bar"
|
className="App-bottom-bar"
|
||||||
style={{
|
style={{
|
||||||
|
@ -202,20 +205,11 @@ export const MobileMenu = ({
|
||||||
{appState.collaborators.size > 0 && (
|
{appState.collaborators.size > 0 && (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.collaborators")}</legend>
|
<legend>{t("labels.collaborators")}</legend>
|
||||||
<UserList mobile>
|
<UserList
|
||||||
{Array.from(appState.collaborators)
|
mobile
|
||||||
// Collaborator is either not initialized or is actually the current user.
|
collaborators={appState.collaborators}
|
||||||
.filter(
|
actionManager={actionManager}
|
||||||
([_, client]) => Object.keys(client).length !== 0,
|
/>
|
||||||
)
|
|
||||||
.map(([clientId, client]) => (
|
|
||||||
<React.Fragment key={clientId}>
|
|
||||||
{actionManager.renderAction("goToCollaborator", {
|
|
||||||
id: clientId,
|
|
||||||
})}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</UserList>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
)}
|
)}
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import React, { useState, useLayoutEffect, useRef } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { useExcalidrawContainer, useDeviceType } from "./App";
|
import { useExcalidrawContainer, useDevice } from "./App";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { THEME } from "../constants";
|
import { THEME } from "../constants";
|
||||||
|
|
||||||
|
@ -59,17 +59,17 @@ export const Modal = (props: {
|
||||||
const useBodyRoot = (theme: AppState["theme"]) => {
|
const useBodyRoot = (theme: AppState["theme"]) => {
|
||||||
const [div, setDiv] = useState<HTMLDivElement | null>(null);
|
const [div, setDiv] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const deviceType = useDeviceType();
|
const device = useDevice();
|
||||||
const isMobileRef = useRef(deviceType.isMobile);
|
const isMobileRef = useRef(device.isMobile);
|
||||||
isMobileRef.current = deviceType.isMobile;
|
isMobileRef.current = device.isMobile;
|
||||||
|
|
||||||
const { container: excalidrawContainer } = useExcalidrawContainer();
|
const { container: excalidrawContainer } = useExcalidrawContainer();
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (div) {
|
if (div) {
|
||||||
div.classList.toggle("excalidraw--mobile", deviceType.isMobile);
|
div.classList.toggle("excalidraw--mobile", device.isMobile);
|
||||||
}
|
}
|
||||||
}, [div, deviceType.isMobile]);
|
}, [div, device.isMobile]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const isDarkTheme =
|
const isDarkTheme =
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import React, { useLayoutEffect, useRef, useEffect } from "react";
|
import React, { useLayoutEffect, useRef, useEffect } from "react";
|
||||||
import "./Popover.scss";
|
import "./Popover.scss";
|
||||||
import { unstable_batchedUpdates } from "react-dom";
|
import { unstable_batchedUpdates } from "react-dom";
|
||||||
|
import { queryFocusableElements } from "../utils";
|
||||||
|
import { KEYS } from "../keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
top?: number;
|
top?: number;
|
||||||
|
@ -27,6 +29,41 @@ export const Popover = ({
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const popoverRef = useRef<HTMLDivElement>(null);
|
const popoverRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const container = popoverRef.current;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === KEYS.TAB) {
|
||||||
|
const focusableElements = queryFocusableElements(container);
|
||||||
|
const { activeElement } = document;
|
||||||
|
const currentIndex = focusableElements.findIndex(
|
||||||
|
(element) => element === activeElement,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentIndex === 0 && event.shiftKey) {
|
||||||
|
focusableElements[focusableElements.length - 1].focus();
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
} else if (
|
||||||
|
currentIndex === focusableElements.length - 1 &&
|
||||||
|
!event.shiftKey
|
||||||
|
) {
|
||||||
|
focusableElements[0].focus();
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
container.addEventListener("keydown", handleKeyDown);
|
||||||
|
|
||||||
|
return () => container.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [container]);
|
||||||
|
|
||||||
// ensure the popover doesn't overflow the viewport
|
// ensure the popover doesn't overflow the viewport
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (fitInViewport && popoverRef.current) {
|
if (fitInViewport && popoverRef.current) {
|
||||||
|
|
22
src/components/SidebarLockButton.scss
Normal file
22
src/components/SidebarLockButton.scss
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
@import "../css/variables.module";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.layer-ui__sidebar-lock-button {
|
||||||
|
@include toolbarButtonColorStates;
|
||||||
|
margin-right: 0.2rem;
|
||||||
|
}
|
||||||
|
.ToolIcon_type_floating .side_lock_icon {
|
||||||
|
width: calc(var(--space-factor) * 7);
|
||||||
|
height: calc(var(--space-factor) * 7);
|
||||||
|
svg {
|
||||||
|
// mirror
|
||||||
|
transform: scale(-1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToolIcon_type_checkbox {
|
||||||
|
&:not(.ToolIcon_toggle_opaque):checked + .side_lock_icon {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
46
src/components/SidebarLockButton.tsx
Normal file
46
src/components/SidebarLockButton.tsx
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import "./ToolIcon.scss";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { ToolButtonSize } from "./ToolButton";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import { Tooltip } from "./Tooltip";
|
||||||
|
|
||||||
|
import "./SidebarLockButton.scss";
|
||||||
|
|
||||||
|
type SidebarLockIconProps = {
|
||||||
|
checked: boolean;
|
||||||
|
onChange?(): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_SIZE: ToolButtonSize = "medium";
|
||||||
|
|
||||||
|
const SIDE_LIBRARY_TOGGLE_ICON = (
|
||||||
|
<svg viewBox="0 0 24 24" fill="#ffffff">
|
||||||
|
<path d="M19 22H5a3 3 0 01-3-3V5a3 3 0 013-3h14a3 3 0 013 3v14a3 3 0 01-3 3zm0-18h-9v16h9a1.01 1.01 0 001-1V5a1.01 1.01 0 00-1-1z"></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SidebarLockButton = (props: SidebarLockIconProps) => {
|
||||||
|
return (
|
||||||
|
<Tooltip label={t("labels.sidebarLock")}>
|
||||||
|
<label
|
||||||
|
className={clsx(
|
||||||
|
"ToolIcon ToolIcon__lock ToolIcon_type_floating",
|
||||||
|
`ToolIcon_size_${DEFAULT_SIZE}`,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className="ToolIcon_type_checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
onChange={props.onChange}
|
||||||
|
checked={props.checked}
|
||||||
|
aria-label={t("labels.sidebarLock")}
|
||||||
|
/>{" "}
|
||||||
|
<div className="ToolIcon__icon side_lock_icon" tabIndex={0}>
|
||||||
|
{SIDE_LIBRARY_TOGGLE_ICON}
|
||||||
|
</div>{" "}
|
||||||
|
</label>{" "}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
|
@ -41,6 +41,7 @@ const ColStack = ({
|
||||||
align,
|
align,
|
||||||
justifyContent,
|
justifyContent,
|
||||||
className,
|
className,
|
||||||
|
style,
|
||||||
}: StackProps) => {
|
}: StackProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -49,6 +50,7 @@ const ColStack = ({
|
||||||
"--gap": gap,
|
"--gap": gap,
|
||||||
justifyItems: align,
|
justifyItems: align,
|
||||||
justifyContent,
|
justifyContent,
|
||||||
|
...style,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
right: 12px;
|
right: 12px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
pointer-events: all;
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
margin: 0 24px 8px 0;
|
margin: 0 24px 8px 0;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from "react";
|
||||||
import { getCommonBounds } from "../element/bounds";
|
import { getCommonBounds } from "../element/bounds";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useDeviceType } from "../components/App";
|
import { useDevice } from "../components/App";
|
||||||
import { getTargetElements } from "../scene";
|
import { getTargetElements } from "../scene";
|
||||||
import { AppState, ExcalidrawProps } from "../types";
|
import { AppState, ExcalidrawProps } from "../types";
|
||||||
import { close } from "./icons";
|
import { close } from "./icons";
|
||||||
|
@ -16,16 +16,13 @@ export const Stats = (props: {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
renderCustomStats: ExcalidrawProps["renderCustomStats"];
|
renderCustomStats: ExcalidrawProps["renderCustomStats"];
|
||||||
}) => {
|
}) => {
|
||||||
const deviceType = useDeviceType();
|
const device = useDevice();
|
||||||
|
|
||||||
const boundingBox = getCommonBounds(props.elements);
|
const boundingBox = getCommonBounds(props.elements);
|
||||||
const selectedElements = getTargetElements(props.elements, props.appState);
|
const selectedElements = getTargetElements(props.elements, props.appState);
|
||||||
const selectedBoundingBox = getCommonBounds(selectedElements);
|
const selectedBoundingBox = getCommonBounds(selectedElements);
|
||||||
|
if (device.isMobile && props.appState.openMenu) {
|
||||||
if (deviceType.isMobile && props.appState.openMenu) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Stats">
|
<div className="Stats">
|
||||||
<Island padding={2}>
|
<Island padding={2}>
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.Toast {
|
.Toast {
|
||||||
|
$closeButtonSize: 1.2rem;
|
||||||
|
$closeButtonPadding: 0.4rem;
|
||||||
|
|
||||||
animation: fade-in 0.5s;
|
animation: fade-in 0.5s;
|
||||||
background-color: var(--button-gray-1);
|
background-color: var(--button-gray-1);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
@ -15,11 +18,24 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
z-index: 999999;
|
z-index: 999999;
|
||||||
}
|
|
||||||
|
|
||||||
.Toast__message {
|
.Toast__message {
|
||||||
color: var(--popup-text-color);
|
padding: 0 $closeButtonSize + ($closeButtonPadding);
|
||||||
white-space: pre-wrap;
|
color: var(--popup-text-color);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: $closeButtonPadding;
|
||||||
|
|
||||||
|
.ToolIcon__icon {
|
||||||
|
width: $closeButtonSize;
|
||||||
|
height: $closeButtonSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fade-in {
|
@keyframes fade-in {
|
||||||
|
|
|
@ -1,34 +1,59 @@
|
||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
import { TOAST_TIMEOUT } from "../constants";
|
import { close } from "./icons";
|
||||||
import "./Toast.scss";
|
import "./Toast.scss";
|
||||||
|
import { ToolButton } from "./ToolButton";
|
||||||
|
|
||||||
|
const DEFAULT_TOAST_TIMEOUT = 5000;
|
||||||
|
|
||||||
export const Toast = ({
|
export const Toast = ({
|
||||||
message,
|
message,
|
||||||
clearToast,
|
clearToast,
|
||||||
|
closable = false,
|
||||||
|
// To prevent autoclose, pass duration as Infinity
|
||||||
|
duration = DEFAULT_TOAST_TIMEOUT,
|
||||||
}: {
|
}: {
|
||||||
message: string;
|
message: string;
|
||||||
clearToast: () => void;
|
clearToast: () => void;
|
||||||
|
closable?: boolean;
|
||||||
|
duration?: number;
|
||||||
}) => {
|
}) => {
|
||||||
const timerRef = useRef<number>(0);
|
const timerRef = useRef<number>(0);
|
||||||
|
const shouldAutoClose = duration !== Infinity;
|
||||||
const scheduleTimeout = useCallback(
|
const scheduleTimeout = useCallback(() => {
|
||||||
() =>
|
if (!shouldAutoClose) {
|
||||||
(timerRef.current = window.setTimeout(() => clearToast(), TOAST_TIMEOUT)),
|
return;
|
||||||
[clearToast],
|
}
|
||||||
);
|
timerRef.current = window.setTimeout(() => clearToast(), duration);
|
||||||
|
}, [clearToast, duration, shouldAutoClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!shouldAutoClose) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
scheduleTimeout();
|
scheduleTimeout();
|
||||||
return () => clearTimeout(timerRef.current);
|
return () => clearTimeout(timerRef.current);
|
||||||
}, [scheduleTimeout, message]);
|
}, [scheduleTimeout, message, duration, shouldAutoClose]);
|
||||||
|
|
||||||
|
const onMouseEnter = shouldAutoClose
|
||||||
|
? () => clearTimeout(timerRef?.current)
|
||||||
|
: undefined;
|
||||||
|
const onMouseLeave = shouldAutoClose ? scheduleTimeout : undefined;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="Toast"
|
className="Toast"
|
||||||
onMouseEnter={() => clearTimeout(timerRef?.current)}
|
onMouseEnter={onMouseEnter}
|
||||||
onMouseLeave={scheduleTimeout}
|
onMouseLeave={onMouseLeave}
|
||||||
>
|
>
|
||||||
<p className="Toast__message">{message}</p>
|
<p className="Toast__message">{message}</p>
|
||||||
|
{closable && (
|
||||||
|
<ToolButton
|
||||||
|
icon={close}
|
||||||
|
aria-label="close"
|
||||||
|
type="icon"
|
||||||
|
onClick={clearToast}
|
||||||
|
className="close"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,26 +1,5 @@
|
||||||
@import "open-color/open-color.scss";
|
@import "open-color/open-color.scss";
|
||||||
|
@import "../css/variables.module";
|
||||||
@mixin toolbarButtonColorStates {
|
|
||||||
.ToolIcon_type_radio,
|
|
||||||
.ToolIcon_type_checkbox {
|
|
||||||
& + .ToolIcon__icon:active {
|
|
||||||
background: var(--color-primary-light);
|
|
||||||
}
|
|
||||||
&:checked + .ToolIcon__icon {
|
|
||||||
background: var(--color-primary);
|
|
||||||
--icon-fill-color: #{$oc-white};
|
|
||||||
--keybinding-color: #{$oc-white};
|
|
||||||
}
|
|
||||||
&:checked + .ToolIcon__icon:active {
|
|
||||||
background: var(--color-primary-darker);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ToolIcon__keybinding {
|
|
||||||
bottom: 4px;
|
|
||||||
right: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.App-toolbar-container {
|
.App-toolbar-container {
|
||||||
|
|
|
@ -2,17 +2,51 @@ import "./UserList.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { AppState, Collaborator } from "../types";
|
||||||
|
import { Tooltip } from "./Tooltip";
|
||||||
|
import { ActionManager } from "../actions/manager";
|
||||||
|
|
||||||
type UserListProps = {
|
export const UserList: React.FC<{
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
mobile?: boolean;
|
mobile?: boolean;
|
||||||
};
|
collaborators: AppState["collaborators"];
|
||||||
|
actionManager: ActionManager;
|
||||||
|
}> = ({ className, mobile, collaborators, actionManager }) => {
|
||||||
|
const uniqueCollaborators = new Map<string, Collaborator>();
|
||||||
|
|
||||||
|
collaborators.forEach((collaborator, socketId) => {
|
||||||
|
uniqueCollaborators.set(
|
||||||
|
// filter on user id, else fall back on unique socketId
|
||||||
|
collaborator.id || socketId,
|
||||||
|
collaborator,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const avatars =
|
||||||
|
uniqueCollaborators.size > 0 &&
|
||||||
|
Array.from(uniqueCollaborators)
|
||||||
|
.filter(([_, client]) => Object.keys(client).length !== 0)
|
||||||
|
.map(([clientId, collaborator]) => {
|
||||||
|
const avatarJSX = actionManager.renderAction("goToCollaborator", [
|
||||||
|
clientId,
|
||||||
|
collaborator,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return mobile ? (
|
||||||
|
<Tooltip
|
||||||
|
label={collaborator.username || "Unknown user"}
|
||||||
|
key={clientId}
|
||||||
|
>
|
||||||
|
{avatarJSX}
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<React.Fragment key={clientId}>{avatarJSX}</React.Fragment>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export const UserList = ({ children, className, mobile }: UserListProps) => {
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx("UserList", className, { UserList_mobile: mobile })}>
|
<div className={clsx("UserList", className, { UserList_mobile: mobile })}>
|
||||||
{children}
|
{avatars}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -116,7 +116,6 @@ export const IMAGE_RENDER_TIMEOUT = 500;
|
||||||
export const TAP_TWICE_TIMEOUT = 300;
|
export const TAP_TWICE_TIMEOUT = 300;
|
||||||
export const TOUCH_CTX_MENU_TIMEOUT = 500;
|
export const TOUCH_CTX_MENU_TIMEOUT = 500;
|
||||||
export const TITLE_TIMEOUT = 10000;
|
export const TITLE_TIMEOUT = 10000;
|
||||||
export const TOAST_TIMEOUT = 5000;
|
|
||||||
export const VERSION_TIMEOUT = 30000;
|
export const VERSION_TIMEOUT = 30000;
|
||||||
export const SCROLL_TIMEOUT = 100;
|
export const SCROLL_TIMEOUT = 100;
|
||||||
export const ZOOM_STEP = 0.1;
|
export const ZOOM_STEP = 0.1;
|
||||||
|
@ -155,9 +154,19 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// breakpoints
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// sm screen
|
||||||
|
export const MQ_SM_MAX_WIDTH = 640;
|
||||||
|
// md screen
|
||||||
export const MQ_MAX_WIDTH_PORTRAIT = 730;
|
export const MQ_MAX_WIDTH_PORTRAIT = 730;
|
||||||
export const MQ_MAX_WIDTH_LANDSCAPE = 1000;
|
export const MQ_MAX_WIDTH_LANDSCAPE = 1000;
|
||||||
export const MQ_MAX_HEIGHT_LANDSCAPE = 500;
|
export const MQ_MAX_HEIGHT_LANDSCAPE = 500;
|
||||||
|
// sidebar
|
||||||
|
export const MQ_RIGHT_SIDEBAR_MIN_WIDTH = 1229;
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const LIBRARY_SIDEBAR_WIDTH = parseInt(cssVariables.rightSidebarWidth);
|
||||||
|
|
||||||
export const MAX_DECIMALS_FOR_SVG_EXPORT = 2;
|
export const MAX_DECIMALS_FOR_SVG_EXPORT = 2;
|
||||||
|
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export const createInverseContext = <T extends unknown = null>(
|
|
||||||
initialValue: T,
|
|
||||||
) => {
|
|
||||||
const Context = React.createContext(initialValue) as React.Context<T> & {
|
|
||||||
_updateProviderValue?: (value: T) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
class InverseConsumer extends React.Component {
|
|
||||||
state = { value: initialValue };
|
|
||||||
constructor(props: any) {
|
|
||||||
super(props);
|
|
||||||
Context._updateProviderValue = (value: T) => this.setState({ value });
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Context.Provider value={this.state.value}>
|
|
||||||
{this.props.children}
|
|
||||||
</Context.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class InverseProvider extends React.Component<{ value: T }> {
|
|
||||||
componentDidMount() {
|
|
||||||
Context._updateProviderValue?.(this.props.value);
|
|
||||||
}
|
|
||||||
componentDidUpdate() {
|
|
||||||
Context._updateProviderValue?.(this.props.value);
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
return <Context.Consumer>{() => this.props.children}</Context.Consumer>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
Context,
|
|
||||||
Consumer: InverseConsumer,
|
|
||||||
Provider: InverseProvider,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -350,7 +350,6 @@
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
z-index: 100;
|
|
||||||
|
|
||||||
:root[dir="ltr"] & {
|
:root[dir="ltr"] & {
|
||||||
left: 0.25rem;
|
left: 0.25rem;
|
||||||
|
@ -391,6 +390,7 @@
|
||||||
|
|
||||||
.App-menu__left {
|
.App-menu__left {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
box-shadow: var(--shadow-island);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-select {
|
.dropdown-select {
|
||||||
|
@ -449,6 +449,7 @@
|
||||||
bottom: 30px;
|
bottom: 30px;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.help-icon {
|
.help-icon {
|
||||||
|
@ -567,6 +568,22 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// use custom, minimalistic scrollbar
|
||||||
|
// (doesn't work in Firefox)
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--button-gray-2);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--button-gray-3);
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:active {
|
||||||
|
background: var(--button-gray-2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ErrorSplash.excalidraw {
|
.ErrorSplash.excalidraw {
|
||||||
|
|
|
@ -6,8 +6,32 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin toolbarButtonColorStates {
|
||||||
|
.ToolIcon_type_radio,
|
||||||
|
.ToolIcon_type_checkbox {
|
||||||
|
& + .ToolIcon__icon:active {
|
||||||
|
background: var(--color-primary-light);
|
||||||
|
}
|
||||||
|
&:checked + .ToolIcon__icon {
|
||||||
|
background: var(--color-primary);
|
||||||
|
--icon-fill-color: #{$oc-white};
|
||||||
|
--keybinding-color: #{$oc-white};
|
||||||
|
}
|
||||||
|
&:checked + .ToolIcon__icon:active {
|
||||||
|
background: var(--color-primary-darker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToolIcon__keybinding {
|
||||||
|
bottom: 4px;
|
||||||
|
right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$theme-filter: "invert(93%) hue-rotate(180deg)";
|
$theme-filter: "invert(93%) hue-rotate(180deg)";
|
||||||
|
$right-sidebar-width: "302px";
|
||||||
|
|
||||||
:export {
|
:export {
|
||||||
themeFilter: unquote($theme-filter);
|
themeFilter: unquote($theme-filter);
|
||||||
|
rightSidebarWidth: unquote($right-sidebar-width);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import {
|
import {
|
||||||
FileWithHandle,
|
|
||||||
fileOpen as _fileOpen,
|
fileOpen as _fileOpen,
|
||||||
fileSave as _fileSave,
|
fileSave as _fileSave,
|
||||||
FileSystemHandle,
|
FileSystemHandle,
|
||||||
|
@ -26,13 +25,9 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
|
||||||
extensions?: FILE_EXTENSION[];
|
extensions?: FILE_EXTENSION[];
|
||||||
description: string;
|
description: string;
|
||||||
multiple?: M;
|
multiple?: M;
|
||||||
}): Promise<
|
}): Promise<M extends false | undefined ? File : File[]> => {
|
||||||
M extends false | undefined ? FileWithHandle : FileWithHandle[]
|
|
||||||
> => {
|
|
||||||
// an unsafe TS hack, alas not much we can do AFAIK
|
// an unsafe TS hack, alas not much we can do AFAIK
|
||||||
type RetType = M extends false | undefined
|
type RetType = M extends false | undefined ? File : File[];
|
||||||
? FileWithHandle
|
|
||||||
: FileWithHandle[];
|
|
||||||
|
|
||||||
const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => {
|
const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => {
|
||||||
mimeTypes.push(MIME_TYPES[type]);
|
mimeTypes.push(MIME_TYPES[type]);
|
||||||
|
|
|
@ -98,7 +98,12 @@ export const loadFromJSON = async (
|
||||||
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
|
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
|
||||||
// extensions: ["json", "excalidraw", "png", "svg"],
|
// extensions: ["json", "excalidraw", "png", "svg"],
|
||||||
});
|
});
|
||||||
return loadFromBlob(await normalizeFile(file), localAppState, localElements);
|
return loadFromBlob(
|
||||||
|
await normalizeFile(file),
|
||||||
|
localAppState,
|
||||||
|
localElements,
|
||||||
|
file.handle,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isValidExcalidrawData = (data?: {
|
export const isValidExcalidrawData = (data?: {
|
||||||
|
|
|
@ -283,6 +283,11 @@ export const restoreAppState = (
|
||||||
value: appState.zoom as NormalizedZoomValue,
|
value: appState.zoom as NormalizedZoomValue,
|
||||||
}
|
}
|
||||||
: appState.zoom || defaultAppState.zoom,
|
: appState.zoom || defaultAppState.zoom,
|
||||||
|
// when sidebar docked and user left it open in last session,
|
||||||
|
// keep it open. If not docked, keep it closed irrespective of last state.
|
||||||
|
isLibraryOpen: nextAppState.isLibraryMenuDocked
|
||||||
|
? nextAppState.isLibraryOpen
|
||||||
|
: false,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -115,6 +115,9 @@ describe("textWysiwyg", () => {
|
||||||
height: textSize,
|
height: textSize,
|
||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
});
|
});
|
||||||
|
mutateElement(container, {
|
||||||
|
boundElements: [{ type: "text", id: text.id }],
|
||||||
|
});
|
||||||
|
|
||||||
h.elements = [container, text];
|
h.elements = [container, text];
|
||||||
|
|
||||||
|
|
|
@ -8,10 +8,12 @@ import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
InitializedExcalidrawImageElement,
|
InitializedExcalidrawImageElement,
|
||||||
} from "../../element/types";
|
} from "../../element/types";
|
||||||
import { getSceneVersion } from "../../packages/excalidraw/index";
|
import {
|
||||||
|
getSceneVersion,
|
||||||
|
restoreElements,
|
||||||
|
} from "../../packages/excalidraw/index";
|
||||||
import { Collaborator, Gesture } from "../../types";
|
import { Collaborator, Gesture } from "../../types";
|
||||||
import {
|
import {
|
||||||
getFrame,
|
|
||||||
preventUnload,
|
preventUnload,
|
||||||
resolvablePromise,
|
resolvablePromise,
|
||||||
withBatchedUpdates,
|
withBatchedUpdates,
|
||||||
|
@ -47,11 +49,9 @@ import {
|
||||||
} from "../data/localStorage";
|
} from "../data/localStorage";
|
||||||
import Portal from "./Portal";
|
import Portal from "./Portal";
|
||||||
import RoomDialog from "./RoomDialog";
|
import RoomDialog from "./RoomDialog";
|
||||||
import { createInverseContext } from "../../createInverseContext";
|
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import { UserIdleState } from "../../types";
|
import { UserIdleState } from "../../types";
|
||||||
import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../constants";
|
import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../constants";
|
||||||
import { trackEvent } from "../../analytics";
|
|
||||||
import {
|
import {
|
||||||
encodeFilesForUpload,
|
encodeFilesForUpload,
|
||||||
FileManager,
|
FileManager,
|
||||||
|
@ -70,52 +70,45 @@ import {
|
||||||
import { decryptData } from "../../data/encryption";
|
import { decryptData } from "../../data/encryption";
|
||||||
import { resetBrowserStateVersions } from "../data/tabSync";
|
import { resetBrowserStateVersions } from "../data/tabSync";
|
||||||
import { LocalData } from "../data/LocalData";
|
import { LocalData } from "../data/LocalData";
|
||||||
|
import { atom, useAtom } from "jotai";
|
||||||
|
import { jotaiStore } from "../../jotai";
|
||||||
|
|
||||||
|
export const collabAPIAtom = atom<CollabAPI | null>(null);
|
||||||
|
export const collabDialogShownAtom = atom(false);
|
||||||
|
export const isCollaboratingAtom = atom(false);
|
||||||
|
|
||||||
interface CollabState {
|
interface CollabState {
|
||||||
modalIsShown: boolean;
|
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
username: string;
|
username: string;
|
||||||
userState: UserIdleState;
|
|
||||||
activeRoomLink: string;
|
activeRoomLink: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type CollabInstance = InstanceType<typeof CollabWrapper>;
|
type CollabInstance = InstanceType<typeof Collab>;
|
||||||
|
|
||||||
export interface CollabAPI {
|
export interface CollabAPI {
|
||||||
/** function so that we can access the latest value from stale callbacks */
|
/** function so that we can access the latest value from stale callbacks */
|
||||||
isCollaborating: () => boolean;
|
isCollaborating: () => boolean;
|
||||||
username: CollabState["username"];
|
|
||||||
userState: CollabState["userState"];
|
|
||||||
onPointerUpdate: CollabInstance["onPointerUpdate"];
|
onPointerUpdate: CollabInstance["onPointerUpdate"];
|
||||||
initializeSocketClient: CollabInstance["initializeSocketClient"];
|
startCollaboration: CollabInstance["startCollaboration"];
|
||||||
onCollabButtonClick: CollabInstance["onCollabButtonClick"];
|
stopCollaboration: CollabInstance["stopCollaboration"];
|
||||||
syncElements: CollabInstance["syncElements"];
|
syncElements: CollabInstance["syncElements"];
|
||||||
fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"];
|
fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"];
|
||||||
setUsername: (username: string) => void;
|
setUsername: (username: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface PublicProps {
|
||||||
excalidrawAPI: ExcalidrawImperativeAPI;
|
excalidrawAPI: ExcalidrawImperativeAPI;
|
||||||
onRoomClose?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
type Props = PublicProps & { modalIsShown: boolean };
|
||||||
Context: CollabContext,
|
|
||||||
Consumer: CollabContextConsumer,
|
|
||||||
Provider: CollabContextProvider,
|
|
||||||
} = createInverseContext<{ api: CollabAPI | null }>({ api: null });
|
|
||||||
|
|
||||||
export { CollabContext, CollabContextConsumer };
|
class Collab extends PureComponent<Props, CollabState> {
|
||||||
|
|
||||||
class CollabWrapper extends PureComponent<Props, CollabState> {
|
|
||||||
portal: Portal;
|
portal: Portal;
|
||||||
fileManager: FileManager;
|
fileManager: FileManager;
|
||||||
excalidrawAPI: Props["excalidrawAPI"];
|
excalidrawAPI: Props["excalidrawAPI"];
|
||||||
activeIntervalId: number | null;
|
activeIntervalId: number | null;
|
||||||
idleTimeoutId: number | null;
|
idleTimeoutId: number | null;
|
||||||
|
|
||||||
// marked as private to ensure we don't change it outside this class
|
|
||||||
private _isCollaborating: boolean = false;
|
|
||||||
private socketInitializationTimer?: number;
|
private socketInitializationTimer?: number;
|
||||||
private lastBroadcastedOrReceivedSceneVersion: number = -1;
|
private lastBroadcastedOrReceivedSceneVersion: number = -1;
|
||||||
private collaborators = new Map<string, Collaborator>();
|
private collaborators = new Map<string, Collaborator>();
|
||||||
|
@ -123,10 +116,8 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
modalIsShown: false,
|
|
||||||
errorMessage: "",
|
errorMessage: "",
|
||||||
username: importUsernameFromLocalStorage() || "",
|
username: importUsernameFromLocalStorage() || "",
|
||||||
userState: UserIdleState.ACTIVE,
|
|
||||||
activeRoomLink: "",
|
activeRoomLink: "",
|
||||||
};
|
};
|
||||||
this.portal = new Portal(this);
|
this.portal = new Portal(this);
|
||||||
|
@ -164,6 +155,18 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||||
window.addEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
|
window.addEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
|
||||||
window.addEventListener(EVENT.UNLOAD, this.onUnload);
|
window.addEventListener(EVENT.UNLOAD, this.onUnload);
|
||||||
|
|
||||||
|
const collabAPI: CollabAPI = {
|
||||||
|
isCollaborating: this.isCollaborating,
|
||||||
|
onPointerUpdate: this.onPointerUpdate,
|
||||||
|
startCollaboration: this.startCollaboration,
|
||||||
|
syncElements: this.syncElements,
|
||||||
|
fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase,
|
||||||
|
stopCollaboration: this.stopCollaboration,
|
||||||
|
setUsername: this.setUsername,
|
||||||
|
};
|
||||||
|
|
||||||
|
jotaiStore.set(collabAPIAtom, collabAPI);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
process.env.NODE_ENV === ENV.TEST ||
|
process.env.NODE_ENV === ENV.TEST ||
|
||||||
process.env.NODE_ENV === ENV.DEVELOPMENT
|
process.env.NODE_ENV === ENV.DEVELOPMENT
|
||||||
|
@ -196,7 +199,11 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isCollaborating = () => this._isCollaborating;
|
isCollaborating = () => jotaiStore.get(isCollaboratingAtom)!;
|
||||||
|
|
||||||
|
private setIsCollaborating = (isCollaborating: boolean) => {
|
||||||
|
jotaiStore.set(isCollaboratingAtom, isCollaborating);
|
||||||
|
};
|
||||||
|
|
||||||
private onUnload = () => {
|
private onUnload = () => {
|
||||||
this.destroySocketClient({ isUnload: true });
|
this.destroySocketClient({ isUnload: true });
|
||||||
|
@ -208,7 +215,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this._isCollaborating &&
|
this.isCollaborating() &&
|
||||||
(this.fileManager.shouldPreventUnload(syncableElements) ||
|
(this.fileManager.shouldPreventUnload(syncableElements) ||
|
||||||
!isSavedToFirebase(this.portal, syncableElements))
|
!isSavedToFirebase(this.portal, syncableElements))
|
||||||
) {
|
) {
|
||||||
|
@ -252,12 +259,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
openPortal = async () => {
|
stopCollaboration = (keepRemoteState = true) => {
|
||||||
trackEvent("share", "room creation", `ui (${getFrame()})`);
|
|
||||||
return this.initializeSocketClient(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
closePortal = () => {
|
|
||||||
this.queueBroadcastAllElements.cancel();
|
this.queueBroadcastAllElements.cancel();
|
||||||
this.queueSaveToFirebase.cancel();
|
this.queueSaveToFirebase.cancel();
|
||||||
this.loadImageFiles.cancel();
|
this.loadImageFiles.cancel();
|
||||||
|
@ -267,16 +269,26 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||||
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
|
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (window.confirm(t("alerts.collabStopOverridePrompt"))) {
|
|
||||||
|
if (this.portal.socket && this.fallbackInitializationHandler) {
|
||||||
|
this.portal.socket.off(
|
||||||
|
"connect_error",
|
||||||
|
this.fallbackInitializationHandler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!keepRemoteState) {
|
||||||
|
LocalData.fileStorage.reset();
|
||||||
|
this.destroySocketClient();
|
||||||
|
} else if (window.confirm(t("alerts.collabStopOverridePrompt"))) {
|
||||||
// hack to ensure that we prefer we disregard any new browser state
|
// hack to ensure that we prefer we disregard any new browser state
|
||||||
// that could have been saved in other tabs while we were collaborating
|
// that could have been saved in other tabs while we were collaborating
|
||||||
resetBrowserStateVersions();
|
resetBrowserStateVersions();
|
||||||
|
|
||||||
window.history.pushState({}, APP_NAME, window.location.origin);
|
window.history.pushState({}, APP_NAME, window.location.origin);
|
||||||
this.destroySocketClient();
|
this.destroySocketClient();
|
||||||
trackEvent("share", "room closed");
|
|
||||||
|
|
||||||
this.props.onRoomClose?.();
|
LocalData.fileStorage.reset();
|
||||||
|
|
||||||
const elements = this.excalidrawAPI
|
const elements = this.excalidrawAPI
|
||||||
.getSceneElementsIncludingDeleted()
|
.getSceneElementsIncludingDeleted()
|
||||||
|
@ -295,20 +307,20 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private destroySocketClient = (opts?: { isUnload: boolean }) => {
|
private destroySocketClient = (opts?: { isUnload: boolean }) => {
|
||||||
|
this.lastBroadcastedOrReceivedSceneVersion = -1;
|
||||||
|
this.portal.close();
|
||||||
|
this.fileManager.reset();
|
||||||
if (!opts?.isUnload) {
|
if (!opts?.isUnload) {
|
||||||
|
this.setIsCollaborating(false);
|
||||||
|
this.setState({
|
||||||
|
activeRoomLink: "",
|
||||||
|
});
|
||||||
this.collaborators = new Map();
|
this.collaborators = new Map();
|
||||||
this.excalidrawAPI.updateScene({
|
this.excalidrawAPI.updateScene({
|
||||||
collaborators: this.collaborators,
|
collaborators: this.collaborators,
|
||||||
});
|
});
|
||||||
this.setState({
|
|
||||||
activeRoomLink: "",
|
|
||||||
});
|
|
||||||
this._isCollaborating = false;
|
|
||||||
LocalData.resumeSave("collaboration");
|
LocalData.resumeSave("collaboration");
|
||||||
}
|
}
|
||||||
this.lastBroadcastedOrReceivedSceneVersion = -1;
|
|
||||||
this.portal.close();
|
|
||||||
this.fileManager.reset();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private fetchImageFilesFromFirebase = async (scene: {
|
private fetchImageFilesFromFirebase = async (scene: {
|
||||||
|
@ -349,7 +361,9 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private initializeSocketClient = async (
|
private fallbackInitializationHandler: null | (() => any) = null;
|
||||||
|
|
||||||
|
startCollaboration = async (
|
||||||
existingRoomLinkData: null | { roomId: string; roomKey: string },
|
existingRoomLinkData: null | { roomId: string; roomKey: string },
|
||||||
): Promise<ImportedDataState | null> => {
|
): Promise<ImportedDataState | null> => {
|
||||||
if (this.portal.socket) {
|
if (this.portal.socket) {
|
||||||
|
@ -372,13 +386,23 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||||
|
|
||||||
const scenePromise = resolvablePromise<ImportedDataState | null>();
|
const scenePromise = resolvablePromise<ImportedDataState | null>();
|
||||||
|
|
||||||
this._isCollaborating = true;
|
this.setIsCollaborating(true);
|
||||||
LocalData.pauseSave("collaboration");
|
LocalData.pauseSave("collaboration");
|
||||||
|
|
||||||
const { default: socketIOClient } = await import(
|
const { default: socketIOClient } = await import(
|
||||||
/* webpackChunkName: "socketIoClient" */ "socket.io-client"
|
/* webpackChunkName: "socketIoClient" */ "socket.io-client"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const fallbackInitializationHandler = () => {
|
||||||
|
this.initializeRoom({
|
||||||
|
roomLinkData: existingRoomLinkData,
|
||||||
|
fetchScene: true,
|
||||||
|
}).then((scene) => {
|
||||||
|
scenePromise.resolve(scene);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
this.fallbackInitializationHandler = fallbackInitializationHandler;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const socketServerData = await getCollabServer();
|
const socketServerData = await getCollabServer();
|
||||||
|
|
||||||
|
@ -391,6 +415,8 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||||
roomId,
|
roomId,
|
||||||
roomKey,
|
roomKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.portal.socket.once("connect_error", fallbackInitializationHandler);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
this.setState({ errorMessage: error.message });
|
this.setState({ errorMessage: error.message });
|
||||||
|
@ -419,13 +445,10 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||||
|
|
||||||
// fallback in case you're not alone in the room but still don't receive
|
// fallback in case you're not alone in the room but still don't receive
|
||||||
// initial SCENE_INIT message
|
// initial SCENE_INIT message
|
||||||
this.socketInitializationTimer = window.setTimeout(() => {
|
this.socketInitializationTimer = window.setTimeout(
|
||||||
this.initializeRoom({
|
fallbackInitializationHandler,
|
||||||
roomLinkData: existingRoomLinkData,
|
INITIAL_SCENE_UPDATE_TIMEOUT,
|
||||||
fetchScene: true,
|
);
|
||||||
});
|
|
||||||
scenePromise.resolve(null);
|
|
||||||
}, INITIAL_SCENE_UPDATE_TIMEOUT);
|
|
||||||
|
|
||||||
// All socket listeners are moving to Portal
|
// All socket listeners are moving to Portal
|
||||||
this.portal.socket.on(
|
this.portal.socket.on(
|
||||||
|
@ -530,6 +553,12 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||||
}
|
}
|
||||||
| { fetchScene: false; roomLinkData?: null }) => {
|
| { fetchScene: false; roomLinkData?: null }) => {
|
||||||
clearTimeout(this.socketInitializationTimer!);
|
clearTimeout(this.socketInitializationTimer!);
|
||||||
|
if (this.portal.socket && this.fallbackInitializationHandler) {
|
||||||
|
this.portal.socket.off(
|
||||||
|
"connect_error",
|
||||||
|
this.fallbackInitializationHandler,
|
||||||
|
);
|
||||||
|
}
|
||||||
if (fetchScene && roomLinkData && this.portal.socket) {
|
if (fetchScene && roomLinkData && this.portal.socket) {
|
||||||
this.excalidrawAPI.resetScene();
|
this.excalidrawAPI.resetScene();
|
||||||
|
|
||||||
|
@ -567,6 +596,8 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||||
const localElements = this.getSceneElementsIncludingDeleted();
|
const localElements = this.getSceneElementsIncludingDeleted();
|
||||||
const appState = this.excalidrawAPI.getAppState();
|
const appState = this.excalidrawAPI.getAppState();
|
||||||
|
|
||||||
|
remoteElements = restoreElements(remoteElements, null);
|
||||||
|
|
||||||
const reconciledElements = _reconcileElements(
|
const reconciledElements = _reconcileElements(
|
||||||
localElements,
|
localElements,
|
||||||
remoteElements,
|
remoteElements,
|
||||||
|
@ -672,19 +703,17 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
setCollaborators(sockets: string[]) {
|
setCollaborators(sockets: string[]) {
|
||||||
this.setState((state) => {
|
const collaborators: InstanceType<typeof Collab>["collaborators"] =
|
||||||
const collaborators: InstanceType<typeof CollabWrapper>["collaborators"] =
|
new Map();
|
||||||
new Map();
|
for (const socketId of sockets) {
|
||||||
for (const socketId of sockets) {
|
if (this.collaborators.has(socketId)) {
|
||||||
if (this.collaborators.has(socketId)) {
|
collaborators.set(socketId, this.collaborators.get(socketId)!);
|
||||||
collaborators.set(socketId, this.collaborators.get(socketId)!);
|
} else {
|
||||||
} else {
|
collaborators.set(socketId, {});
|
||||||
collaborators.set(socketId, {});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.collaborators = collaborators;
|
}
|
||||||
this.excalidrawAPI.updateScene({ collaborators });
|
this.collaborators = collaborators;
|
||||||
});
|
this.excalidrawAPI.updateScene({ collaborators });
|
||||||
}
|
}
|
||||||
|
|
||||||
public setLastBroadcastedOrReceivedSceneVersion = (version: number) => {
|
public setLastBroadcastedOrReceivedSceneVersion = (version: number) => {
|
||||||
|
@ -713,7 +742,6 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||||
);
|
);
|
||||||
|
|
||||||
onIdleStateChange = (userState: UserIdleState) => {
|
onIdleStateChange = (userState: UserIdleState) => {
|
||||||
this.setState({ userState });
|
|
||||||
this.portal.broadcastIdleChange(userState);
|
this.portal.broadcastIdleChange(userState);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -747,18 +775,22 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||||
this.setLastBroadcastedOrReceivedSceneVersion(newVersion);
|
this.setLastBroadcastedOrReceivedSceneVersion(newVersion);
|
||||||
}, SYNC_FULL_SCENE_INTERVAL_MS);
|
}, SYNC_FULL_SCENE_INTERVAL_MS);
|
||||||
|
|
||||||
queueSaveToFirebase = throttle(() => {
|
queueSaveToFirebase = throttle(
|
||||||
if (this.portal.socketInitialized) {
|
() => {
|
||||||
this.saveCollabRoomToFirebase(
|
if (this.portal.socketInitialized) {
|
||||||
getSyncableElements(
|
this.saveCollabRoomToFirebase(
|
||||||
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
|
getSyncableElements(
|
||||||
),
|
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||||
);
|
),
|
||||||
}
|
);
|
||||||
}, SYNC_FULL_SCENE_INTERVAL_MS);
|
}
|
||||||
|
},
|
||||||
|
SYNC_FULL_SCENE_INTERVAL_MS,
|
||||||
|
{ leading: false },
|
||||||
|
);
|
||||||
|
|
||||||
handleClose = () => {
|
handleClose = () => {
|
||||||
this.setState({ modalIsShown: false });
|
jotaiStore.set(collabDialogShownAtom, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
setUsername = (username: string) => {
|
setUsername = (username: string) => {
|
||||||
|
@ -770,35 +802,10 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||||
saveUsernameToLocalStorage(username);
|
saveUsernameToLocalStorage(username);
|
||||||
};
|
};
|
||||||
|
|
||||||
onCollabButtonClick = () => {
|
|
||||||
this.setState({
|
|
||||||
modalIsShown: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/** PRIVATE. Use `this.getContextValue()` instead. */
|
|
||||||
private contextValue: CollabAPI | null = null;
|
|
||||||
|
|
||||||
/** Getter of context value. Returned object is stable. */
|
|
||||||
getContextValue = (): CollabAPI => {
|
|
||||||
if (!this.contextValue) {
|
|
||||||
this.contextValue = {} as CollabAPI;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.contextValue.isCollaborating = this.isCollaborating;
|
|
||||||
this.contextValue.username = this.state.username;
|
|
||||||
this.contextValue.onPointerUpdate = this.onPointerUpdate;
|
|
||||||
this.contextValue.initializeSocketClient = this.initializeSocketClient;
|
|
||||||
this.contextValue.onCollabButtonClick = this.onCollabButtonClick;
|
|
||||||
this.contextValue.syncElements = this.syncElements;
|
|
||||||
this.contextValue.fetchImageFilesFromFirebase =
|
|
||||||
this.fetchImageFilesFromFirebase;
|
|
||||||
this.contextValue.setUsername = this.setUsername;
|
|
||||||
return this.contextValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { modalIsShown, username, errorMessage, activeRoomLink } = this.state;
|
const { username, errorMessage, activeRoomLink } = this.state;
|
||||||
|
|
||||||
|
const { modalIsShown } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -808,8 +815,8 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||||
activeRoomLink={activeRoomLink}
|
activeRoomLink={activeRoomLink}
|
||||||
username={username}
|
username={username}
|
||||||
onUsernameChange={this.onUsernameChange}
|
onUsernameChange={this.onUsernameChange}
|
||||||
onRoomCreate={this.openPortal}
|
onRoomCreate={() => this.startCollaboration(null)}
|
||||||
onRoomDestroy={this.closePortal}
|
onRoomDestroy={this.stopCollaboration}
|
||||||
setErrorMessage={(errorMessage) => {
|
setErrorMessage={(errorMessage) => {
|
||||||
this.setState({ errorMessage });
|
this.setState({ errorMessage });
|
||||||
}}
|
}}
|
||||||
|
@ -822,11 +829,6 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||||
onClose={() => this.setState({ errorMessage: "" })}
|
onClose={() => this.setState({ errorMessage: "" })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<CollabContextProvider
|
|
||||||
value={{
|
|
||||||
api: this.getContextValue(),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -834,7 +836,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
collab: InstanceType<typeof CollabWrapper>;
|
collab: InstanceType<typeof Collab>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -845,4 +847,11 @@ if (
|
||||||
window.collab = window.collab || ({} as Window["collab"]);
|
window.collab = window.collab || ({} as Window["collab"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CollabWrapper;
|
const _Collab: React.FC<PublicProps> = (props) => {
|
||||||
|
const [collabDialogShown] = useAtom(collabDialogShownAtom);
|
||||||
|
return <Collab {...props} modalIsShown={collabDialogShown} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default _Collab;
|
||||||
|
|
||||||
|
export type TCollabClass = Collab;
|
|
@ -4,7 +4,7 @@ import {
|
||||||
SocketUpdateDataSource,
|
SocketUpdateDataSource,
|
||||||
} from "../data";
|
} from "../data";
|
||||||
|
|
||||||
import CollabWrapper from "./CollabWrapper";
|
import { TCollabClass } from "./Collab";
|
||||||
|
|
||||||
import { ExcalidrawElement } from "../../element/types";
|
import { ExcalidrawElement } from "../../element/types";
|
||||||
import {
|
import {
|
||||||
|
@ -20,14 +20,14 @@ import { BroadcastedExcalidrawElement } from "./reconciliation";
|
||||||
import { encryptData } from "../../data/encryption";
|
import { encryptData } from "../../data/encryption";
|
||||||
|
|
||||||
class Portal {
|
class Portal {
|
||||||
collab: CollabWrapper;
|
collab: TCollabClass;
|
||||||
socket: SocketIOClient.Socket | null = null;
|
socket: SocketIOClient.Socket | null = null;
|
||||||
socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initialized
|
socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initialized
|
||||||
roomId: string | null = null;
|
roomId: string | null = null;
|
||||||
roomKey: string | null = null;
|
roomKey: string | null = null;
|
||||||
broadcastedElementVersions: Map<string, number> = new Map();
|
broadcastedElementVersions: Map<string, number> = new Map();
|
||||||
|
|
||||||
constructor(collab: CollabWrapper) {
|
constructor(collab: TCollabClass) {
|
||||||
this.collab = collab;
|
this.collab = collab;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,8 @@ import { t } from "../../i18n";
|
||||||
import "./RoomDialog.scss";
|
import "./RoomDialog.scss";
|
||||||
import Stack from "../../components/Stack";
|
import Stack from "../../components/Stack";
|
||||||
import { AppState } from "../../types";
|
import { AppState } from "../../types";
|
||||||
|
import { trackEvent } from "../../analytics";
|
||||||
|
import { getFrame } from "../../utils";
|
||||||
|
|
||||||
const getShareIcon = () => {
|
const getShareIcon = () => {
|
||||||
const navigator = window.navigator as any;
|
const navigator = window.navigator as any;
|
||||||
|
@ -95,7 +97,10 @@ const RoomDialog = ({
|
||||||
title={t("roomDialog.button_startSession")}
|
title={t("roomDialog.button_startSession")}
|
||||||
aria-label={t("roomDialog.button_startSession")}
|
aria-label={t("roomDialog.button_startSession")}
|
||||||
showAriaLabel={true}
|
showAriaLabel={true}
|
||||||
onClick={onRoomCreate}
|
onClick={() => {
|
||||||
|
trackEvent("share", "room creation", `ui (${getFrame()})`);
|
||||||
|
onRoomCreate();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -160,7 +165,10 @@ const RoomDialog = ({
|
||||||
title={t("roomDialog.button_stopSession")}
|
title={t("roomDialog.button_stopSession")}
|
||||||
aria-label={t("roomDialog.button_stopSession")}
|
aria-label={t("roomDialog.button_stopSession")}
|
||||||
showAriaLabel={true}
|
showAriaLabel={true}
|
||||||
onClick={onRoomDestroy}
|
onClick={() => {
|
||||||
|
trackEvent("share", "room closed");
|
||||||
|
onRoomDestroy();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -134,9 +134,16 @@ export type SocketUpdateData =
|
||||||
_brand: "socketUpdateData";
|
_brand: "socketUpdateData";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RE_COLLAB_LINK = /^#room=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/;
|
||||||
|
|
||||||
|
export const isCollaborationLink = (link: string) => {
|
||||||
|
const hash = new URL(link).hash;
|
||||||
|
return RE_COLLAB_LINK.test(hash);
|
||||||
|
};
|
||||||
|
|
||||||
export const getCollaborationLinkData = (link: string) => {
|
export const getCollaborationLinkData = (link: string) => {
|
||||||
const hash = new URL(link).hash;
|
const hash = new URL(link).hash;
|
||||||
const match = hash.match(/^#room=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/);
|
const match = hash.match(RE_COLLAB_LINK);
|
||||||
if (match && match[2].length !== 22) {
|
if (match && match[2].length !== 22) {
|
||||||
window.alert(t("alerts.invalidEncryptionKey"));
|
window.alert(t("alerts.invalidEncryptionKey"));
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
import { useCallback, useContext, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
import { ErrorDialog } from "../components/ErrorDialog";
|
import { ErrorDialog } from "../components/ErrorDialog";
|
||||||
|
@ -18,7 +18,7 @@ import {
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { useCallbackRefState } from "../hooks/useCallbackRefState";
|
import { useCallbackRefState } from "../hooks/useCallbackRefState";
|
||||||
import { Language, t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import {
|
import {
|
||||||
Excalidraw,
|
Excalidraw,
|
||||||
defaultLang,
|
defaultLang,
|
||||||
|
@ -45,20 +45,26 @@ import {
|
||||||
STORAGE_KEYS,
|
STORAGE_KEYS,
|
||||||
SYNC_BROWSER_TABS_TIMEOUT,
|
SYNC_BROWSER_TABS_TIMEOUT,
|
||||||
} from "./app_constants";
|
} from "./app_constants";
|
||||||
import CollabWrapper, {
|
import Collab, {
|
||||||
CollabAPI,
|
CollabAPI,
|
||||||
CollabContext,
|
collabAPIAtom,
|
||||||
CollabContextConsumer,
|
collabDialogShownAtom,
|
||||||
} from "./collab/CollabWrapper";
|
isCollaboratingAtom,
|
||||||
|
} from "./collab/Collab";
|
||||||
import { LanguageList } from "./components/LanguageList";
|
import { LanguageList } from "./components/LanguageList";
|
||||||
import { exportToBackend, getCollaborationLinkData, loadScene } from "./data";
|
import {
|
||||||
|
exportToBackend,
|
||||||
|
getCollaborationLinkData,
|
||||||
|
isCollaborationLink,
|
||||||
|
loadScene,
|
||||||
|
} from "./data";
|
||||||
import {
|
import {
|
||||||
getLibraryItemsFromStorage,
|
getLibraryItemsFromStorage,
|
||||||
importFromLocalStorage,
|
importFromLocalStorage,
|
||||||
importUsernameFromLocalStorage,
|
importUsernameFromLocalStorage,
|
||||||
} from "./data/localStorage";
|
} from "./data/localStorage";
|
||||||
import CustomStats from "./CustomStats";
|
import CustomStats from "./CustomStats";
|
||||||
import { restoreAppState, RestoredDataState } from "../data/restore";
|
import { restore, restoreAppState, RestoredDataState } from "../data/restore";
|
||||||
import { Tooltip } from "../components/Tooltip";
|
import { Tooltip } from "../components/Tooltip";
|
||||||
import { shield } from "../components/icons";
|
import { shield } from "../components/icons";
|
||||||
|
|
||||||
|
@ -72,6 +78,9 @@ import { loadFilesFromFirebase } from "./data/firebase";
|
||||||
import { LocalData } from "./data/LocalData";
|
import { LocalData } from "./data/LocalData";
|
||||||
import { isBrowserStorageStateNewer } from "./data/tabSync";
|
import { isBrowserStorageStateNewer } from "./data/tabSync";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { Provider, useAtom } from "jotai";
|
||||||
|
import { jotaiStore, useAtomWithInitialValue } from "../jotai";
|
||||||
|
import { reconcileElements } from "./collab/reconciliation";
|
||||||
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
|
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
|
||||||
|
|
||||||
const isExcalidrawPlusSignedUser = document.cookie.includes(
|
const isExcalidrawPlusSignedUser = document.cookie.includes(
|
||||||
|
@ -80,11 +89,7 @@ const isExcalidrawPlusSignedUser = document.cookie.includes(
|
||||||
|
|
||||||
const languageDetector = new LanguageDetector();
|
const languageDetector = new LanguageDetector();
|
||||||
languageDetector.init({
|
languageDetector.init({
|
||||||
languageUtils: {
|
languageUtils: {},
|
||||||
formatLanguageCode: (langCode: Language["code"]) => langCode,
|
|
||||||
isWhitelisted: () => true,
|
|
||||||
},
|
|
||||||
checkWhitelist: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const initializeScene = async (opts: {
|
const initializeScene = async (opts: {
|
||||||
|
@ -174,7 +179,7 @@ const initializeScene = async (opts: {
|
||||||
|
|
||||||
if (roomLinkData) {
|
if (roomLinkData) {
|
||||||
return {
|
return {
|
||||||
scene: await opts.collabAPI.initializeSocketClient(roomLinkData),
|
scene: await opts.collabAPI.startCollaboration(roomLinkData),
|
||||||
isExternalScene: true,
|
isExternalScene: true,
|
||||||
id: roomLinkData.roomId,
|
id: roomLinkData.roomId,
|
||||||
key: roomLinkData.roomKey,
|
key: roomLinkData.roomKey,
|
||||||
|
@ -246,7 +251,11 @@ const ExcalidrawWrapper = () => {
|
||||||
const [excalidrawAPI, excalidrawRefCallback] =
|
const [excalidrawAPI, excalidrawRefCallback] =
|
||||||
useCallbackRefState<ExcalidrawImperativeAPI>();
|
useCallbackRefState<ExcalidrawImperativeAPI>();
|
||||||
|
|
||||||
const collabAPI = useContext(CollabContext)?.api;
|
const [collabAPI] = useAtom(collabAPIAtom);
|
||||||
|
const [, setCollabDialogShown] = useAtom(collabDialogShownAtom);
|
||||||
|
const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
|
||||||
|
return isCollaborationLink(window.location.href);
|
||||||
|
});
|
||||||
|
|
||||||
useHandleLibrary({
|
useHandleLibrary({
|
||||||
excalidrawAPI,
|
excalidrawAPI,
|
||||||
|
@ -324,21 +333,44 @@ const ExcalidrawWrapper = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
initializeScene({ collabAPI }).then((data) => {
|
initializeScene({ collabAPI }).then(async (data) => {
|
||||||
loadImages(data, /* isInitialLoad */ true);
|
loadImages(data, /* isInitialLoad */ true);
|
||||||
initialStatePromiseRef.current.promise.resolve(data.scene);
|
|
||||||
|
initialStatePromiseRef.current.promise.resolve({
|
||||||
|
...data.scene,
|
||||||
|
// at this point the state may have already been updated (e.g. when
|
||||||
|
// collaborating, we may have received updates from other clients)
|
||||||
|
appState: restoreAppState(
|
||||||
|
data.scene?.appState,
|
||||||
|
excalidrawAPI.getAppState(),
|
||||||
|
),
|
||||||
|
elements: reconcileElements(
|
||||||
|
data.scene?.elements || [],
|
||||||
|
excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||||
|
excalidrawAPI.getAppState(),
|
||||||
|
),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const onHashChange = async (event: HashChangeEvent) => {
|
const onHashChange = async (event: HashChangeEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const libraryUrlTokens = parseLibraryTokensFromUrl();
|
const libraryUrlTokens = parseLibraryTokensFromUrl();
|
||||||
if (!libraryUrlTokens) {
|
if (!libraryUrlTokens) {
|
||||||
|
if (
|
||||||
|
collabAPI.isCollaborating() &&
|
||||||
|
!isCollaborationLink(window.location.href)
|
||||||
|
) {
|
||||||
|
collabAPI.stopCollaboration(false);
|
||||||
|
}
|
||||||
|
excalidrawAPI.updateScene({ appState: { isLoading: true } });
|
||||||
|
|
||||||
initializeScene({ collabAPI }).then((data) => {
|
initializeScene({ collabAPI }).then((data) => {
|
||||||
loadImages(data);
|
loadImages(data);
|
||||||
if (data.scene) {
|
if (data.scene) {
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
...data.scene,
|
...data.scene,
|
||||||
appState: restoreAppState(data.scene.appState, null),
|
...restore(data.scene, null, null),
|
||||||
|
commitToHistory: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -479,19 +511,17 @@ const ExcalidrawWrapper = () => {
|
||||||
if (excalidrawAPI) {
|
if (excalidrawAPI) {
|
||||||
let didChange = false;
|
let didChange = false;
|
||||||
|
|
||||||
let pendingImageElement = appState.pendingImageElement;
|
|
||||||
const elements = excalidrawAPI
|
const elements = excalidrawAPI
|
||||||
.getSceneElementsIncludingDeleted()
|
.getSceneElementsIncludingDeleted()
|
||||||
.map((element) => {
|
.map((element) => {
|
||||||
if (
|
if (
|
||||||
LocalData.fileStorage.shouldUpdateImageElementStatus(element)
|
LocalData.fileStorage.shouldUpdateImageElementStatus(element)
|
||||||
) {
|
) {
|
||||||
didChange = true;
|
const newElement = newElementWith(element, { status: "saved" });
|
||||||
const newEl = newElementWith(element, { status: "saved" });
|
if (newElement !== element) {
|
||||||
if (pendingImageElement === element) {
|
didChange = true;
|
||||||
pendingImageElement = newEl;
|
|
||||||
}
|
}
|
||||||
return newEl;
|
return newElement;
|
||||||
}
|
}
|
||||||
return element;
|
return element;
|
||||||
});
|
});
|
||||||
|
@ -499,9 +529,6 @@ const ExcalidrawWrapper = () => {
|
||||||
if (didChange) {
|
if (didChange) {
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements,
|
elements,
|
||||||
appState: {
|
|
||||||
pendingImageElement,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -645,23 +672,19 @@ const ExcalidrawWrapper = () => {
|
||||||
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
|
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRoomClose = useCallback(() => {
|
|
||||||
LocalData.fileStorage.reset();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{ height: "100%" }}
|
style={{ height: "100%" }}
|
||||||
className={clsx("excalidraw-app", {
|
className={clsx("excalidraw-app", {
|
||||||
"is-collaborating": collabAPI?.isCollaborating(),
|
"is-collaborating": isCollaborating,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Excalidraw
|
<Excalidraw
|
||||||
ref={excalidrawRefCallback}
|
ref={excalidrawRefCallback}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
initialData={initialStatePromiseRef.current.promise}
|
initialData={initialStatePromiseRef.current.promise}
|
||||||
onCollabButtonClick={collabAPI?.onCollabButtonClick}
|
onCollabButtonClick={() => setCollabDialogShown(true)}
|
||||||
isCollaborating={collabAPI?.isCollaborating()}
|
isCollaborating={isCollaborating}
|
||||||
onPointerUpdate={collabAPI?.onPointerUpdate}
|
onPointerUpdate={collabAPI?.onPointerUpdate}
|
||||||
UIOptions={{
|
UIOptions={{
|
||||||
canvasActions: {
|
canvasActions: {
|
||||||
|
@ -695,12 +718,7 @@ const ExcalidrawWrapper = () => {
|
||||||
onLibraryChange={onLibraryChange}
|
onLibraryChange={onLibraryChange}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
{excalidrawAPI && (
|
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
|
||||||
<CollabWrapper
|
|
||||||
excalidrawAPI={excalidrawAPI}
|
|
||||||
onRoomClose={onRoomClose}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<ErrorDialog
|
<ErrorDialog
|
||||||
message={errorMessage}
|
message={errorMessage}
|
||||||
|
@ -714,9 +732,9 @@ const ExcalidrawWrapper = () => {
|
||||||
const ExcalidrawApp = () => {
|
const ExcalidrawApp = () => {
|
||||||
return (
|
return (
|
||||||
<TopErrorBoundary>
|
<TopErrorBoundary>
|
||||||
<CollabContextConsumer>
|
<Provider unstable_createStore={() => jotaiStore}>
|
||||||
<ExcalidrawWrapper />
|
<ExcalidrawWrapper />
|
||||||
</CollabContextConsumer>
|
</Provider>
|
||||||
</TopErrorBoundary>
|
</TopErrorBoundary>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
25
src/jotai.ts
25
src/jotai.ts
|
@ -1,4 +1,27 @@
|
||||||
import { unstable_createStore } from "jotai";
|
import { unstable_createStore, useAtom, WritableAtom } from "jotai";
|
||||||
|
import { useLayoutEffect } from "react";
|
||||||
|
|
||||||
export const jotaiScope = Symbol();
|
export const jotaiScope = Symbol();
|
||||||
export const jotaiStore = unstable_createStore();
|
export const jotaiStore = unstable_createStore();
|
||||||
|
|
||||||
|
export const useAtomWithInitialValue = <
|
||||||
|
T extends unknown,
|
||||||
|
A extends WritableAtom<T, T>,
|
||||||
|
>(
|
||||||
|
atom: A,
|
||||||
|
initialValue: T | (() => T),
|
||||||
|
) => {
|
||||||
|
const [value, setValue] = useAtom(atom);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (typeof initialValue === "function") {
|
||||||
|
// @ts-ignore
|
||||||
|
setValue(initialValue());
|
||||||
|
} else {
|
||||||
|
setValue(initialValue);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return [value, setValue] as const;
|
||||||
|
};
|
||||||
|
|
|
@ -120,7 +120,13 @@
|
||||||
"lockAll": "Lock all",
|
"lockAll": "Lock all",
|
||||||
"unlockAll": "Unlock all"
|
"unlockAll": "Unlock all"
|
||||||
},
|
},
|
||||||
"statusPublished": "Published"
|
"statusPublished": "Published",
|
||||||
|
"sidebarLock": "Keep sidebar open"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"noItems": "No items added yet...",
|
||||||
|
"hint_emptyLibrary": "Select an item on canvas to add it here, or install a library from the public repository, below.",
|
||||||
|
"hint_emptyPrivateLibrary": "Select an item on canvas to add it here."
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Reset the canvas",
|
"clearReset": "Reset the canvas",
|
||||||
|
|
|
@ -17,6 +17,10 @@ Please add the latest change on the top under the correct section.
|
||||||
|
|
||||||
#### Features
|
#### Features
|
||||||
|
|
||||||
|
- Add `[UIOptions.dockedSidebarBreakpoint]`(https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#dockedSidebarBreakpoint) to customize at which point to break from the docked sidebar [#5274](https://github.com/excalidraw/excalidraw/pull/5274).
|
||||||
|
|
||||||
|
- Added support for supplying user `id` in the Collaborator object (see `collaborators` in [`updateScene()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#updateScene)), which will be used to deduplicate users when rendering collaborator avatar list. Cursors will still be rendered for every user. [#5309](https://github.com/excalidraw/excalidraw/pull/5309)
|
||||||
|
|
||||||
- Export API to [set](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#setCursor) and [reset](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#resetCursor) mouse cursor on the canvas [#5215](https://github.com/excalidraw/excalidraw/pull/5215).
|
- Export API to [set](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#setCursor) and [reset](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#resetCursor) mouse cursor on the canvas [#5215](https://github.com/excalidraw/excalidraw/pull/5215).
|
||||||
|
|
||||||
- Export [`sceneCoordsToViewportCoords`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onPointerDown) and [`viewportCoordsToSceneCoords`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onPointerDown) utilities [#5187](https://github.com/excalidraw/excalidraw/pull/5187).
|
- Export [`sceneCoordsToViewportCoords`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onPointerDown) and [`viewportCoordsToSceneCoords`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onPointerDown) utilities [#5187](https://github.com/excalidraw/excalidraw/pull/5187).
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
#### Note
|
||||||
|
|
||||||
|
⚠️ ⚠️ ⚠️ You are viewing the docs for the **next** release, in case you want to check the docs for the stable release, you can view it [here](https://www.npmjs.com/package/@excalidraw/excalidraw).
|
||||||
|
|
||||||
### Excalidraw
|
### Excalidraw
|
||||||
|
|
||||||
Excalidraw exported as a component to directly embed in your projects.
|
Excalidraw exported as a component to directly embed in your projects.
|
||||||
|
@ -26,7 +30,7 @@ If you want to load assets from a different path you can set a variable `window.
|
||||||
|
|
||||||
#### Note
|
#### Note
|
||||||
|
|
||||||
**If you don't want to wait for the next stable release and try out the unreleased changes you can use [@excalidraw/excalidraw-next](https://www.npmjs.com/package/@excalidraw/excalidraw-next).**
|
**If you don't want to wait for the next stable release and try out the unreleased changes you can use `@excalidraw/excalidraw@next`.**
|
||||||
|
|
||||||
### Demo
|
### Demo
|
||||||
|
|
||||||
|
@ -42,7 +46,7 @@ If you are using a Web bundler (for instance, Webpack), you can import it as an
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import React, { useEffect, useState, useRef } from "react";
|
import React, { useEffect, useState, useRef } from "react";
|
||||||
import Excalidraw from "@excalidraw/excalidraw";
|
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||||
import InitialData from "./initialData";
|
import InitialData from "./initialData";
|
||||||
|
|
||||||
import "./styles.scss";
|
import "./styles.scss";
|
||||||
|
@ -322,7 +326,7 @@ const App = () => {
|
||||||
className: "excalidraw-wrapper",
|
className: "excalidraw-wrapper",
|
||||||
ref: excalidrawWrapperRef,
|
ref: excalidrawWrapperRef,
|
||||||
},
|
},
|
||||||
React.createElement(Excalidraw.default, {
|
React.createElement(ExcalidrawLib.Excalidraw, {
|
||||||
initialData: InitialData,
|
initialData: InitialData,
|
||||||
onChange: (elements, state) =>
|
onChange: (elements, state) =>
|
||||||
console.log("Elements :", elements, "State : ", state),
|
console.log("Elements :", elements, "State : ", state),
|
||||||
|
@ -377,7 +381,7 @@ For a complete list of variables, check [theme.scss](https://github.com/excalidr
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| [`onChange`](#onChange) | Function | | This callback is triggered whenever the component updates due to any change. This callback will receive the excalidraw elements and the current app state. |
|
| [`onChange`](#onChange) | Function | | This callback is triggered whenever the component updates due to any change. This callback will receive the excalidraw elements and the current app state. |
|
||||||
| [`initialData`](#initialData) | <pre>{elements?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>, appState?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState<a> } </pre> | null | The initial data with which app loads. |
|
| [`initialData`](#initialData) | <pre>{elements?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a>, appState?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState<a> } </pre> | null | The initial data with which app loads. |
|
||||||
| [`ref`](#ref) | [`createRef`](https://reactjs.org/docs/refs-and-the-dom.html#creating-refs) | [`useRef`](https://reactjs.org/docs/hooks-reference.html#useref) | [`callbackRef`](https://reactjs.org/docs/refs-and-the-dom.html#callback-refs) | <pre>{ current: { readyPromise: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L317">resolvablePromise</a> } }</pre> | | Ref to be passed to Excalidraw |
|
| [`ref`](#ref) | [`createRef`](https://reactjs.org/docs/refs-and-the-dom.html#creating-refs) | [`useRef`](https://reactjs.org/docs/hooks-reference.html#useref) | [`callbackRef`](https://reactjs.org/docs/refs-and-the-dom.html#callback-refs) | <pre>{ current: { readyPromise: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L317">resolvablePromise</a> } }</pre> | | Ref to be passed to Excalidraw |
|
||||||
| [`onCollabButtonClick`](#onCollabButtonClick) | Function | | Callback to be triggered when the collab button is clicked |
|
| [`onCollabButtonClick`](#onCollabButtonClick) | Function | | Callback to be triggered when the collab button is clicked |
|
||||||
| [`isCollaborating`](#isCollaborating) | `boolean` | | This implies if the app is in collaboration mode |
|
| [`isCollaborating`](#isCollaborating) | `boolean` | | This implies if the app is in collaboration mode |
|
||||||
|
@ -399,7 +403,9 @@ For a complete list of variables, check [theme.scss](https://github.com/excalidr
|
||||||
| [`onLibraryChange`](#onLibraryChange) | <pre>(items: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>) => void | Promise<any> </pre> | | The callback if supplied is triggered when the library is updated and receives the library items. |
|
| [`onLibraryChange`](#onLibraryChange) | <pre>(items: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>) => void | Promise<any> </pre> | | The callback if supplied is triggered when the library is updated and receives the library items. |
|
||||||
| [`autoFocus`](#autoFocus) | boolean | false | Implies whether to focus the Excalidraw component on page load |
|
| [`autoFocus`](#autoFocus) | boolean | false | Implies whether to focus the Excalidraw component on page load |
|
||||||
| [`generateIdForFile`](#generateIdForFile) | `(file: File) => string | Promise<string>` | Allows you to override `id` generation for files added on canvas |
|
| [`generateIdForFile`](#generateIdForFile) | `(file: File) => string | Promise<string>` | Allows you to override `id` generation for files added on canvas |
|
||||||
| [`onLinkOpen`](#onLinkOpen) | <pre>(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">NonDeletedExcalidrawElement</a>, event: CustomEvent) </pre> | | This prop if passed will be triggered when link of an element is clicked |
|
| [`onLinkOpen`](#onLinkOpen) | <pre>(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">NonDeletedExcalidrawElement</a>, event: CustomEvent) </pre> | | This prop if passed will be triggered when link of an element is clicked. |
|
||||||
|
| [`onPointerDown`](#onPointerDown) | <pre>(activeTool: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L93"> AppState["activeTool"]</a>, pointerDownState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L365">PointerDownState</a>) => void</pre> | | This prop if passed gets triggered on pointer down evenets |
|
||||||
|
| [`onScrollChange`](#onScrollChange) | (scrollX: number, scrollY: number) | | This prop if passed gets triggered when scrolling the canvas. |
|
||||||
|
|
||||||
### Dimensions of Excalidraw
|
### Dimensions of Excalidraw
|
||||||
|
|
||||||
|
@ -413,9 +419,9 @@ Every time component updates, this callback if passed will get triggered and has
|
||||||
(excalidrawElements, appState, files) => void;
|
(excalidrawElements, appState, files) => void;
|
||||||
```
|
```
|
||||||
|
|
||||||
1.`excalidrawElements`: Array of [excalidrawElements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78) in the scene.
|
1.`excalidrawElements`: Array of [excalidrawElements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106) in the scene.
|
||||||
|
|
||||||
2.`appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42) of the scene.
|
2.`appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79) of the scene.
|
||||||
|
|
||||||
3. `files`: The [`BinaryFiles`]([BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) which are added to the scene.
|
3. `files`: The [`BinaryFiles`]([BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) which are added to the scene.
|
||||||
|
|
||||||
|
@ -427,10 +433,10 @@ This helps to load Excalidraw with `initialData`. It must be an object or a [pro
|
||||||
|
|
||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `elements` | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78) | The elements with which Excalidraw should be mounted. |
|
| `elements` | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106) | The elements with which Excalidraw should be mounted. |
|
||||||
| `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42) | The App state with which Excalidraw should be mounted. |
|
| `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79) | The App state with which Excalidraw should be mounted. |
|
||||||
| `scrollToContent` | boolean | This attribute implies whether to scroll to the nearest element to center once Excalidraw is mounted. By default, it will not scroll the nearest element to the center. Make sure you pass `initialData.appState.scrollX` and `initialData.appState.scrollY` when `scrollToContent` is false so that scroll positions are retained |
|
| `scrollToContent` | boolean | This attribute implies whether to scroll to the nearest element to center once Excalidraw is mounted. By default, it will not scroll the nearest element to the center. Make sure you pass `initialData.appState.scrollX` and `initialData.appState.scrollY` when `scrollToContent` is false so that scroll positions are retained |
|
||||||
| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L151) | This library items with which Excalidraw should be mounted. |
|
| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200) | Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)> | This library items with which Excalidraw should be mounted. |
|
||||||
| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) | The files added to the scene. |
|
| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) | The files added to the scene. |
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
@ -472,19 +478,23 @@ You can pass a `ref` when you want to access some excalidraw APIs. We expose the
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| ready | `boolean` | This is set to true once Excalidraw is rendered |
|
| ready | `boolean` | This is set to true once Excalidraw is rendered |
|
||||||
| readyPromise | [resolvablePromise](https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L317) | This promise will be resolved with the api once excalidraw has rendered. This will be helpful when you want do some action on the host app once this promise resolves. For this to work you will have to pass ref as shown [here](#readyPromise) |
|
| readyPromise | [resolvablePromise](https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L317) | This promise will be resolved with the api once excalidraw has rendered. This will be helpful when you want do some action on the host app once this promise resolves. For this to work you will have to pass ref as shown [here](#readyPromise) |
|
||||||
| [updateScene](#updateScene) | <pre>(scene: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L207">sceneData</a>) => void </pre> | updates the scene with the sceneData |
|
| [updateScene](#updateScene) | <code>(scene: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L207">sceneData</a>) => void </code> | updates the scene with the sceneData |
|
||||||
| [addFiles](#addFiles) | <pre>(files: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts">BinaryFileData</a>) => void </pre> | add files data to the appState |
|
| [updateLibrary](#updateLibrary) | <code>(<a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/library.ts#L136">opts</a>) => Promise<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>> </code> | updates the scene with the sceneData |
|
||||||
|
| [addFiles](#addFiles) | <code>(files: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts">BinaryFileData</a>) => void </code> | add files data to the appState |
|
||||||
| resetScene | `({ resetLoadingState: boolean }) => void` | Resets the scene. If `resetLoadingState` is passed as true then it will also force set the loading state to false. |
|
| resetScene | `({ resetLoadingState: boolean }) => void` | Resets the scene. If `resetLoadingState` is passed as true then it will also force set the loading state to false. |
|
||||||
| getSceneElementsIncludingDeleted | <pre> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a></pre> | Returns all the elements including the deleted in the scene |
|
| getSceneElementsIncludingDeleted | <code> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a></code> | Returns all the elements including the deleted in the scene |
|
||||||
| getSceneElements | <pre> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a></pre> | Returns all the elements excluding the deleted in the scene |
|
| getSceneElements | <code> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a></code> | Returns all the elements excluding the deleted in the scene |
|
||||||
| getAppState | <pre> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState</a></pre> | Returns current appState |
|
| getAppState | <code> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L66">AppState</a></code> | Returns current appState |
|
||||||
| history | `{ clear: () => void }` | This is the history API. `history.clear()` will clear the history |
|
| history | `{ clear: () => void }` | This is the history API. `history.clear()` will clear the history |
|
||||||
| scrollToContent | <pre> (target?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement</a> | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement</a>[]) => void </pre> | Scroll the nearest element out of the elements supplied to the center. Defaults to the elements on the scene. |
|
| scrollToContent | <code> (target?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement</a> | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement</a>[]) => void </code> | Scroll the nearest element out of the elements supplied to the center. Defaults to the elements on the scene. |
|
||||||
| refresh | `() => void` | Updates the offsets for the Excalidraw component so that the coordinates are computed correctly (for example the cursor position). You don't have to call this when the position is changed on page scroll or when the excalidraw container resizes (we handle that ourselves). For any other cases if the position of excalidraw is updated (example due to scroll on parent container and not page scroll) you should call this API. |
|
| refresh | `() => void` | Updates the offsets for the Excalidraw component so that the coordinates are computed correctly (for example the cursor position). You don't have to call this when the position is changed on page scroll or when the excalidraw container resizes (we handle that ourselves). For any other cases if the position of excalidraw is updated (example due to scroll on parent container and not page scroll) you should call this API. |
|
||||||
| [importLibrary](#importlibrary) | `(url: string, token?: string) => void` | Imports library from given URL |
|
| [importLibrary](#importlibrary) | `(url: string, token?: string) => void` | Imports library from given URL |
|
||||||
| setToastMessage | `(message: string) => void` | This API can be used to show the toast with custom message. |
|
| setToastMessage | `(message: string) => void` | This API can be used to show the toast with custom message. |
|
||||||
| [id](#id) | string | Unique ID for the excalidraw component. |
|
| [id](#id) | string | Unique ID for the excalidraw component. |
|
||||||
| [getFiles](#getFiles) | <pre>() => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64">files</a> </pre> | This API can be used to get the files present in the scene. It may contain files that aren't referenced by any element, so if you're persisting the files to a storage, you should compare them against stored elements. |
|
| [getFiles](#getFiles) | <code>() => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64">files</a> </code> | This API can be used to get the files present in the scene. It may contain files that aren't referenced by any element, so if you're persisting the files to a storage, you should compare them against stored elements. |
|
||||||
|
| [setActiveTool](#setActiveTool) | <code>(tool: { type: typeof <a href="https://github.com/excalidraw/excalidraw/blob/master/src/shapes.tsx#L4">SHAPES</a>[number]["value"] | "eraser" } | { type: "custom"; customType: string }) => void</code> | This API can be used to set the active tool |
|
||||||
|
| [setCursor](#setCursor) | <code>(cursor: string) => void </code> | This API can be used to set customise the mouse cursor on the canvas |
|
||||||
|
| [resetCursor](#resetCursor) | <code>() => void </code> | This API can be used to reset to default mouse cursor on the canvas |
|
||||||
|
|
||||||
#### `readyPromise`
|
#### `readyPromise`
|
||||||
|
|
||||||
|
@ -506,9 +516,31 @@ You can use this function to update the scene with the sceneData. It accepts the
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `elements` | [`ImportedDataState["elements"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L17) | The `elements` to be updated in the scene |
|
| `elements` | [`ImportedDataState["elements"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L17) | The `elements` to be updated in the scene |
|
||||||
| `appState` | [`ImportedDataState["appState"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L18) | The `appState` to be updated in the scene. |
|
| `appState` | [`ImportedDataState["appState"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L18) | The `appState` to be updated in the scene. |
|
||||||
| `collaborators` | <pre>Map<string, <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L29">Collaborator></a></pre> | The list of collaborators to be updated in the scene. |
|
| `collaborators` | <pre>Map<string, <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L35">Collaborator></a></pre> | The list of collaborators to be updated in the scene. |
|
||||||
| `commitToHistory` | `boolean` | Implies if the `history (undo/redo)` should be recorded. Defaults to `false`. |
|
| `commitToHistory` | `boolean` | Implies if the `history (undo/redo)` should be recorded. Defaults to `false`. |
|
||||||
| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L258) | The `libraryItems` to be update in the scene. |
|
| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200) | Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)> | ((currentItems: [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)>) => [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200) | Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)>) | The `libraryItems` to be update in the scene. |
|
||||||
|
|
||||||
|
### `updateLibrary`
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
(opts: {
|
||||||
|
libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L224">LibraryItemsSource</a>;
|
||||||
|
merge?: boolean;
|
||||||
|
prompt?: boolean;
|
||||||
|
openLibraryMenu?: boolean;
|
||||||
|
defaultStatus?: "unpublished" | "published";
|
||||||
|
}) => Promise<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
You can use this function to update the library. It accepts the below attributes.
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `libraryItems` | | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L224) | The `libraryItems` to be replaced/merged with current library |
|
||||||
|
| `merge` | boolean | `false` | Whether to merge with existing library items. |
|
||||||
|
| `prompt` | boolean | `false` | Whether to prompt user for confirmation. |
|
||||||
|
| `openLibraryMenu` | boolean | `false` | Whether to open the library menu before importing. |
|
||||||
|
| `defaultStatus` | <code>"unpublished" | "published"</code> | `"unpublished"` | Default library item's `status` if not present. |
|
||||||
|
|
||||||
### `addFiles`
|
### `addFiles`
|
||||||
|
|
||||||
|
@ -543,7 +575,7 @@ This callback is triggered when mouse pointer is updated.
|
||||||
```
|
```
|
||||||
|
|
||||||
1. `exportedElements`: An array of [non deleted elements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L87) which needs to be exported.
|
1. `exportedElements`: An array of [non deleted elements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L87) which needs to be exported.
|
||||||
2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42) of the scene.
|
2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79) of the scene.
|
||||||
3. `canvas`: The `HTMLCanvasElement` of the scene.
|
3. `canvas`: The `HTMLCanvasElement` of the scene.
|
||||||
|
|
||||||
#### `langCode`
|
#### `langCode`
|
||||||
|
@ -562,7 +594,7 @@ import { defaultLang, languages } from "@excalidraw/excalidraw";
|
||||||
#### `renderTopRightUI`
|
#### `renderTopRightUI`
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
(isMobile: boolean, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState</a>) => JSX
|
(isMobile: boolean, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a>) => JSX | null
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
A function returning JSX to render custom UI in the top right corner of the app.
|
A function returning JSX to render custom UI in the top right corner of the app.
|
||||||
|
@ -570,7 +602,7 @@ A function returning JSX to render custom UI in the top right corner of the app.
|
||||||
#### `renderFooter`
|
#### `renderFooter`
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
(isMobile: boolean, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState</a>) => JSX
|
(isMobile: boolean, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a>) => JSX | null
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
A function returning JSX to render custom UI footer. For example, you can use this to render a language picker that was previously being rendered by Excalidraw itself (for now, you'll need to implement your own language picker).
|
A function returning JSX to render custom UI footer. For example, you can use this to render a language picker that was previously being rendered by Excalidraw itself (for now, you'll need to implement your own language picker).
|
||||||
|
@ -605,7 +637,7 @@ This prop sets the name of the drawing which will be used when exporting the dra
|
||||||
|
|
||||||
#### `UIOptions`
|
#### `UIOptions`
|
||||||
|
|
||||||
This prop can be used to customise UI of Excalidraw. Currently we support customising only [`canvasActions`](#canvasActions). It accepts the below parameters
|
This prop can be used to customise UI of Excalidraw. Currently we support customising [`canvasActions`](#canvasActions) and [`dockedSidebarBreakpoint`](dockedSidebarBreakpoint). It accepts the below parameters
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
{ canvasActions: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L208"> CanvasActions<a/> }
|
{ canvasActions: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L208"> CanvasActions<a/> }
|
||||||
|
@ -623,6 +655,12 @@ This prop can be used to customise UI of Excalidraw. Currently we support custom
|
||||||
| `theme` | boolean | true | Implies whether to show `Theme toggle` |
|
| `theme` | boolean | true | Implies whether to show `Theme toggle` |
|
||||||
| `saveAsImage` | boolean | true | Implies whether to show `Save as image button` |
|
| `saveAsImage` | boolean | true | Implies whether to show `Save as image button` |
|
||||||
|
|
||||||
|
##### `dockedSidebarBreakpoint`
|
||||||
|
|
||||||
|
This prop indicates at what point should we break to a docked, permanent sidebar. If not passed it defaults to [`MQ_RIGHT_SIDEBAR_MAX_WIDTH_PORTRAIT`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L167). If the `width` of the `excalidraw` container exceeds `dockedSidebarBreakpoint`, the sidebar will be dockable. If user choses to dock the sidebar, it will push the right part of the UI towards the left, making space for the sidebar as shown below.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
#### `exportOpts`
|
#### `exportOpts`
|
||||||
|
|
||||||
The below attributes can be set in `UIOptions.canvasActions.export` to customize the export dialog. If `UIOptions.canvasActions.export` is `false` the export button will not be rendered.
|
The below attributes can be set in `UIOptions.canvasActions.export` to customize the export dialog. If `UIOptions.canvasActions.export` is `false` the export button will not be rendered.
|
||||||
|
@ -667,6 +705,26 @@ useEffect(() => {
|
||||||
|
|
||||||
Try out the [Demo](#Demo) to see it in action.
|
Try out the [Demo](#Demo) to see it in action.
|
||||||
|
|
||||||
|
#### `setActiveTool`
|
||||||
|
|
||||||
|
This API has the below signature. It sets the `tool` passed in param as the active tool.
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
(tool: { type: typeof <a href="https://github.com/excalidraw/excalidraw/blob/master/src/shapes.tsx#L4">SHAPES</a>[number]["value"] | "eraser" } | { type: "custom"; customType: string }) => void
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
#### `setCursor`
|
||||||
|
|
||||||
|
This API can be used to customise the mouse cursor on the canvas and has the below signature. It sets the mouse cursor to the cursor passed in param.
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
(cursor: string) => void
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
#### `resetCursor`
|
||||||
|
|
||||||
|
This API can be used to reset to default mouse cursor.
|
||||||
|
|
||||||
#### `detectScroll`
|
#### `detectScroll`
|
||||||
|
|
||||||
Indicates whether Excalidraw should listen for `scroll` event on the nearest scrollable container in the DOM tree and recompute the coordinates (e.g. to correctly handle the cursor) when the component's position changes. You can disable this when you either know this doesn't affect your app or you want to take care of it yourself (calling the [`refresh()`](#ref) method).
|
Indicates whether Excalidraw should listen for `scroll` event on the nearest scrollable container in the DOM tree and recompute the coordinates (e.g. to correctly handle the cursor) when the component's position changes. You can disable this when you either know this doesn't affect your app or you want to take care of it yourself (calling the [`refresh()`](#ref) method).
|
||||||
|
@ -736,6 +794,22 @@ const onLinkOpen: ExcalidrawProps["onLinkOpen"] = useCallback(
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### `onPointerDown`
|
||||||
|
|
||||||
|
This prop if passed will be triggered on pointer down events and has the below signature.
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
(activeTool: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L93"> AppState["activeTool"]</a>, pointerDownState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L365">PointerDownState</a>) => void
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
#### `onScrollChange`
|
||||||
|
|
||||||
|
This prop if passed will be triggered when canvas is scrolled and has the below signature.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
(scrollX: number, scrollY: number) => void
|
||||||
|
```
|
||||||
|
|
||||||
### Does it support collaboration ?
|
### Does it support collaboration ?
|
||||||
|
|
||||||
No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx).
|
No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx).
|
||||||
|
@ -747,7 +821,7 @@ No, Excalidraw package doesn't come with collaboration built in, since the imple
|
||||||
**_Signature_**
|
**_Signature_**
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
restoreAppState(appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L17">ImportedDataState["appState"]</a>, localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState</a>> | null): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState</a>
|
restoreAppState(appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L17">ImportedDataState["appState"]</a>, localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a>> | null): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a>
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
**_How to use_**
|
**_How to use_**
|
||||||
|
@ -756,7 +830,7 @@ restoreAppState(appState: <a href="https://github.com/excalidraw/excalidraw/blob
|
||||||
import { restoreAppState } from "@excalidraw/excalidraw";
|
import { restoreAppState } from "@excalidraw/excalidraw";
|
||||||
```
|
```
|
||||||
|
|
||||||
This function will make sure all the keys have appropriate values in [appState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42) and if any key is missing, it will be set to default value.
|
This function will make sure all the keys have appropriate values in [appState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79) and if any key is missing, it will be set to default value.
|
||||||
|
|
||||||
When `localAppState` is supplied, it's used in place of values that are missing (`undefined`) in `appState` instead of defaults. Use this as a way to not override user's defaults if you persist them. Required: supply `null`/`undefined` if not applicable.
|
When `localAppState` is supplied, it's used in place of values that are missing (`undefined`) in `appState` instead of defaults. Use this as a way to not override user's defaults if you persist them. Required: supply `null`/`undefined` if not applicable.
|
||||||
|
|
||||||
|
@ -765,7 +839,7 @@ When `localAppState` is supplied, it's used in place of values that are missing
|
||||||
**_Signature_**
|
**_Signature_**
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
restoreElements(elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L16">ImportedDataState["elements"]</a>, localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L16">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>
|
restoreElements(elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L16">ImportedDataState["elements"]</a>, localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L16">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a>
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
**_How to use_**
|
**_How to use_**
|
||||||
|
@ -783,7 +857,7 @@ When `localElements` are supplied, they are used to ensure that existing restore
|
||||||
**_Signature_**
|
**_Signature_**
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
restoreElements(data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L12">ImportedDataState</a>, localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState</a>> | null | undefined, localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L16">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a>
|
restoreElements(data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L12">ImportedDataState</a>, localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a>> | null | undefined, localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L16">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a>
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
See [`restoreAppState()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreAppState) about `localAppState`, and [`restoreElements()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreElements) about `localElements`.
|
See [`restoreAppState()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreAppState) about `localAppState`, and [`restoreElements()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreElements) about `localElements`.
|
||||||
|
@ -796,6 +870,24 @@ import { restore } from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
This function makes sure elements and state is set to appropriate values and set to default value if not present. It is a combination of [restoreElements](#restoreElements) and [restoreAppState](#restoreAppState).
|
This function makes sure elements and state is set to appropriate values and set to default value if not present. It is a combination of [restoreElements](#restoreElements) and [restoreAppState](#restoreAppState).
|
||||||
|
|
||||||
|
#### `restoreLibraryItems`
|
||||||
|
|
||||||
|
**_Signature_**
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
restoreLibraryItems(libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L22">ImportedDataState["libraryItems"]</a>, defaultStatus: "published" | "unpublished")
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
**_How to use_**
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { restoreLibraryItems } from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
|
restoreLibraryItems(libraryItems, "unpublished");
|
||||||
|
```
|
||||||
|
|
||||||
|
This function normalizes library items elements, adding missing values when needed.
|
||||||
|
|
||||||
### Export utilities
|
### Export utilities
|
||||||
|
|
||||||
#### `exportToCanvas`
|
#### `exportToCanvas`
|
||||||
|
@ -833,7 +925,7 @@ This function returns the canvas with the exported elements, appState and dimens
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
exportToBlob(
|
exportToBlob(
|
||||||
opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L10">ExportOpts</a> & {
|
opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L14">ExportOpts</a> & {
|
||||||
mimeType?: string,
|
mimeType?: string,
|
||||||
quality?: number;
|
quality?: number;
|
||||||
})
|
})
|
||||||
|
@ -859,8 +951,8 @@ Returns a promise which resolves with a [blob](https://developer.mozilla.org/en-
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
exportToSvg({
|
exportToSvg({
|
||||||
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>,
|
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a>,
|
||||||
appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState</a>,
|
appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a>,
|
||||||
exportPadding?: number,
|
exportPadding?: number,
|
||||||
metadata?: string,
|
metadata?: string,
|
||||||
files?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64">BinaryFiles</a>
|
files?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64">BinaryFiles</a>
|
||||||
|
@ -869,13 +961,41 @@ exportToSvg({
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78) | | The elements to exported as svg |
|
| elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106) | | The elements to exported as svg |
|
||||||
| appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42) | [defaultAppState](https://github.com/excalidraw/excalidraw/blob/master/src/appState.ts#L11) | The app state of the scene |
|
| appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79) | [defaultAppState](https://github.com/excalidraw/excalidraw/blob/master/src/appState.ts#L11) | The app state of the scene |
|
||||||
| exportPadding | number | 10 | The padding to be added on canvas |
|
| exportPadding | number | 10 | The padding to be added on canvas |
|
||||||
| files | [BinaryFiles](The [`BinaryFiles`](<[BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64)>) | undefined | The files added to the scene. |
|
| files | [BinaryFiles](The [`BinaryFiles`](<[BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64)>) | undefined | The files added to the scene. |
|
||||||
|
|
||||||
This function returns a promise which resolves to svg of the exported drawing.
|
This function returns a promise which resolves to svg of the exported drawing.
|
||||||
|
|
||||||
|
#### `exportToClipboard`
|
||||||
|
|
||||||
|
**_Signature_**
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
exportToClipboard(
|
||||||
|
opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L14">ExportOpts</a> & {
|
||||||
|
mimeType?: string,
|
||||||
|
quality?: number;
|
||||||
|
type: 'png' | 'svg' |'json'
|
||||||
|
})
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| opts | | | This param is same as the params passed to `exportToCanvas`. You can refer to [`exportToCanvas`](#exportToCanvas). |
|
||||||
|
| mimeType | string | "image/png" | Indicates the image format, this will be used when exporting as `png`. |
|
||||||
|
| quality | number | 0.92 | A value between 0 and 1 indicating the [image quality](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#parameters). Applies only to `image/jpeg`/`image/webp` MIME types. This will be used when exporting as `png`. |
|
||||||
|
| type | 'png' | 'svg' | 'json' | | This determines the format to which the scene data should be exported. |
|
||||||
|
|
||||||
|
**How to use**
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { exportToClipboard } from "@excalidraw/excalidraw";
|
||||||
|
```
|
||||||
|
|
||||||
|
Copies the scene data in the specified format (determined by `type`) to clipboard.
|
||||||
|
|
||||||
##### Additional attributes of appState for `export\*` APIs
|
##### Additional attributes of appState for `export\*` APIs
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|
@ -883,7 +1003,7 @@ This function returns a promise which resolves to svg of the exported drawing.
|
||||||
| exportBackground | boolean | true | Indicates whether background should be exported |
|
| exportBackground | boolean | true | Indicates whether background should be exported |
|
||||||
| viewBackgroundColor | string | #fff | The default background color |
|
| viewBackgroundColor | string | #fff | The default background color |
|
||||||
| exportWithDarkMode | boolean | false | Indicates whether to export with dark mode |
|
| exportWithDarkMode | boolean | false | Indicates whether to export with dark mode |
|
||||||
| exportEmbedScene | boolean | false | Indicates whether scene data should be embedded in svg. This will increase the svg size. |
|
| exportEmbedScene | boolean | false | Indicates whether scene data should be embedded in svg/png. This will increase the image size. |
|
||||||
|
|
||||||
### Extra API's
|
### Extra API's
|
||||||
|
|
||||||
|
@ -893,20 +1013,35 @@ This function returns a promise which resolves to svg of the exported drawing.
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
serializeAsJSON({
|
serializeAsJSON({
|
||||||
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>,
|
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a>,
|
||||||
appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState</a>,
|
appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a>,
|
||||||
}): string
|
}): string
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
Takes the scene elements and state and returns a JSON string. Deleted `elements`as well as most properties from `AppState` are removed from the resulting JSON. (see [`serializeAsJSON()`](https://github.com/excalidraw/excalidraw/blob/master/src/data/json.ts#L16) source for details).
|
Takes the scene elements and state and returns a JSON string. Deleted `elements`as well as most properties from `AppState` are removed from the resulting JSON. (see [`serializeAsJSON()`](https://github.com/excalidraw/excalidraw/blob/master/src/data/json.ts#L16) source for details).
|
||||||
|
|
||||||
|
If you want to overwrite the source field in the JSON string, you can set `window.EXCALIDRAW_EXPORT_SOURCE` to the desired value.
|
||||||
|
|
||||||
|
#### `serializeLibraryAsJSON`
|
||||||
|
|
||||||
|
**_Signature_**
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
serializeLibraryAsJSON({
|
||||||
|
libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems[]</a>,
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
Takes the library items and returns a JSON string.
|
||||||
|
|
||||||
|
If you want to overwrite the source field in the JSON string, you can set `window.EXCALIDRAW_EXPORT_SOURCE` to the desired value.
|
||||||
|
|
||||||
#### `getSceneVersion`
|
#### `getSceneVersion`
|
||||||
|
|
||||||
**How to use**
|
**How to use**
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
import { getSceneVersion } from "@excalidraw/excalidraw";
|
import { getSceneVersion } from "@excalidraw/excalidraw";
|
||||||
getSceneVersion(elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>)
|
getSceneVersion(elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a>)
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
This function returns the current scene version.
|
This function returns the current scene version.
|
||||||
|
@ -916,7 +1051,7 @@ This function returns the current scene version.
|
||||||
**_Signature_**
|
**_Signature_**
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
isInvisiblySmallElement(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement</a>): boolean
|
isInvisiblySmallElement(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement</a>): boolean
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
**How to use**
|
**How to use**
|
||||||
|
@ -947,15 +1082,51 @@ This function loads the library from the blob.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { loadFromBlob } from "@excalidraw/excalidraw";
|
import { loadFromBlob } from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
|
const scene = await loadFromBlob(file, null, null);
|
||||||
|
excalidrawAPI.updateScene(scene);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Signature**
|
**Signature**
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
loadFromBlob(blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>, localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState</a> | null)
|
loadFromBlob(
|
||||||
|
blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>,
|
||||||
|
localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a> | null,
|
||||||
|
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a> | null,
|
||||||
|
fileHandle?: FileSystemHandle | null
|
||||||
|
) => Promise<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/restore.ts#L53">RestoredDataState</a>>
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
This function loads the scene data from the blob. If you pass `localAppState`, `localAppState` value will be preferred over the `appState` derived from `blob`
|
This function loads the scene data from the blob (or file). If you pass `localAppState`, `localAppState` value will be preferred over the `appState` derived from `blob`. Throws if blob doesn't contain valid scene data.
|
||||||
|
|
||||||
|
#### `loadSceneOrLibraryFromBlob`
|
||||||
|
|
||||||
|
**How to use**
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { loadSceneOrLibraryFromBlob, MIME_TYPES } from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
|
const contents = await loadSceneOrLibraryFromBlob(file, null, null);
|
||||||
|
if (contents.type === MIME_TYPES.excalidraw) {
|
||||||
|
excalidrawAPI.updateScene(contents.data);
|
||||||
|
} else if (contents.type === MIME_TYPES.excalidrawlib) {
|
||||||
|
excalidrawAPI.updateLibrary(contents.data);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Signature**
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
loadSceneOrLibraryFromBlob(
|
||||||
|
blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>,
|
||||||
|
localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a> | null,
|
||||||
|
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a> | null,
|
||||||
|
fileHandle?: FileSystemHandle | null
|
||||||
|
) => Promise<{ type: string, data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/restore.ts#L53">RestoredDataState</a> | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L33">ImportedLibraryState</a>}>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
This function loads either scene or library data from the supplied blob. If the blob contains scene data, and you pass `localAppState`, `localAppState` value will be preferred over the `appState` derived from `blob`. Throws if blob doesn't contain neither valid scene data or library data.
|
||||||
|
|
||||||
#### `getFreeDrawSvgPath`
|
#### `getFreeDrawSvgPath`
|
||||||
|
|
||||||
|
@ -1005,6 +1176,93 @@ getNonDeletedElements(elements: <a href="https://github.com/excalidraw/excalidra
|
||||||
|
|
||||||
This function returns an array of deleted elements.
|
This function returns an array of deleted elements.
|
||||||
|
|
||||||
|
#### `mergeLibraryItems`
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { mergeLibraryItems } from "@excalidraw/excalidraw";
|
||||||
|
```
|
||||||
|
|
||||||
|
**_Signature_**
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
mergeLibraryItems(localItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>, otherItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>) => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
This function merges two `LibraryItems` arrays, where unique items from `otherItems` are sorted first in the returned array.
|
||||||
|
|
||||||
|
#### `parseLibraryTokensFromUrl`
|
||||||
|
|
||||||
|
**How to use**
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { parseLibraryTokensFromUrl } from "@excalidraw/excalidraw";
|
||||||
|
```
|
||||||
|
|
||||||
|
**Signature**
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
parseLibraryTokensFromUrl(): {
|
||||||
|
libraryUrl: string;
|
||||||
|
idToken: string | null;
|
||||||
|
} | null
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
Parses library parameters from URL if present (expects the `#addLibrary` hash key), and returns an object with the `libraryUrl` and `idToken`. Returns `null` if `#addLibrary` hash key not found.
|
||||||
|
|
||||||
|
#### `useHandleLibrary`
|
||||||
|
|
||||||
|
**How to use**
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { useHandleLibrary } from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
|
export const App = () => {
|
||||||
|
// ...
|
||||||
|
useHandleLibrary({ excalidrawAPI });
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Signature**
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
useHandleLibrary(opts: {
|
||||||
|
excalidrawAPI: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L432">ExcalidrawAPI</a>,
|
||||||
|
getInitialLibraryItems?: () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L224">LibraryItemsSource</a>
|
||||||
|
});
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
A hook that automatically imports library from url if `#addLibrary` hash key exists on initial load, or when it changes during the editing session (e.g. when a user installs a new library), and handles initial library load if `getInitialLibraryItems` getter is supplied.
|
||||||
|
|
||||||
|
In the future, we will be adding support for handling library persistence to browser storage (or elsewhere).
|
||||||
|
|
||||||
|
#### `sceneCoordsToViewportCoords`
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { sceneCoordsToViewportCoords } from "@excalidraw/excalidraw";
|
||||||
|
```
|
||||||
|
|
||||||
|
**_Signature_**
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
sceneCoordsToViewportCoords({sceneX: number, sceneY: number}, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a>): {x: number, y: number}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
This function returns equivalent viewport coords for the provided scene coords in params.
|
||||||
|
|
||||||
|
#### `viewportCoordsToSceneCoords`
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { viewportCoordsToSceneCoords } from "@excalidraw/excalidraw";
|
||||||
|
```
|
||||||
|
|
||||||
|
**_Signature_**
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
viewportCoordsToSceneCoords({clientX: number, clientY: number}, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a>): {x: number, y: number}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
This function returns equivalent scene coords for the provided viewport coords in params.
|
||||||
|
|
||||||
### Exported constants
|
### Exported constants
|
||||||
|
|
||||||
#### `FONT_FAMILY`
|
#### `FONT_FAMILY`
|
||||||
|
@ -1042,6 +1300,16 @@ import { THEME } from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
Defaults to `THEME.LIGHT` unless passed in `initialData.appState.theme`
|
Defaults to `THEME.LIGHT` unless passed in `initialData.appState.theme`
|
||||||
|
|
||||||
|
### `MIME_TYPES`
|
||||||
|
|
||||||
|
**How to use **
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { MIME_TYPES } from "@excalidraw/excalidraw";
|
||||||
|
```
|
||||||
|
|
||||||
|
[`MIME_TYPES`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L92) contains all the mime types supported by `Excalidraw`.
|
||||||
|
|
||||||
## Need help?
|
## Need help?
|
||||||
|
|
||||||
Check out the existing [Q&A](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw). If you have any queries or need help, ask us [here](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw).
|
Check out the existing [Q&A](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw). If you have any queries or need help, ask us [here](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw).
|
||||||
|
@ -1069,7 +1337,7 @@ The example is same as the [codesandbox example](https://ehlz3.csb.app/)
|
||||||
You can create a test release by posting the below comment in your pull request
|
You can create a test release by posting the below comment in your pull request
|
||||||
|
|
||||||
```
|
```
|
||||||
@excalibot release package
|
@excalibot trigger release
|
||||||
```
|
```
|
||||||
|
|
||||||
Once the version is released `@excalibot` will post a comment with the release version.
|
Once the version is released `@excalibot` will post a comment with the release version.
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,12 +1,13 @@
|
||||||
import { useEffect, useState, useRef, useCallback } from "react";
|
import { useEffect, useState, useRef, useCallback } from "react";
|
||||||
|
|
||||||
import InitialData from "./initialData";
|
|
||||||
import Sidebar from "./sidebar/Sidebar";
|
import Sidebar from "./sidebar/Sidebar";
|
||||||
|
|
||||||
import "./App.scss";
|
import "./App.scss";
|
||||||
import initialData from "./initialData";
|
import initialData from "./initialData";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import {
|
import {
|
||||||
|
resolvablePromise,
|
||||||
|
ResolvablePromise,
|
||||||
withBatchedUpdates,
|
withBatchedUpdates,
|
||||||
withBatchedUpdatesThrottled,
|
withBatchedUpdatesThrottled,
|
||||||
} from "../../../utils";
|
} from "../../../utils";
|
||||||
|
@ -14,7 +15,42 @@ import { EVENT } from "../../../constants";
|
||||||
import { distance2d } from "../../../math";
|
import { distance2d } from "../../../math";
|
||||||
import { fileOpen } from "../../../data/filesystem";
|
import { fileOpen } from "../../../data/filesystem";
|
||||||
import { loadSceneOrLibraryFromBlob } from "../../utils";
|
import { loadSceneOrLibraryFromBlob } from "../../utils";
|
||||||
|
import {
|
||||||
|
AppState,
|
||||||
|
BinaryFileData,
|
||||||
|
ExcalidrawImperativeAPI,
|
||||||
|
ExcalidrawInitialDataState,
|
||||||
|
Gesture,
|
||||||
|
LibraryItems,
|
||||||
|
PointerDownState as ExcalidrawPointerDownState,
|
||||||
|
} from "../../../types";
|
||||||
|
import { ExcalidrawElement } from "../../../element/types";
|
||||||
|
import { ImportedLibraryData } from "../../../data/types";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
ExcalidrawLib: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Comment = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
value: string;
|
||||||
|
id?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PointerDownState = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
hitElement: Comment;
|
||||||
|
onMove: any;
|
||||||
|
onUp: any;
|
||||||
|
hitElementOffsets: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
// This is so that we use the bundled excalidraw.development.js file instead
|
// This is so that we use the bundled excalidraw.development.js file instead
|
||||||
// of the actual source code
|
// of the actual source code
|
||||||
|
|
||||||
|
@ -28,6 +64,7 @@ const {
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
sceneCoordsToViewportCoords,
|
sceneCoordsToViewportCoords,
|
||||||
viewportCoordsToSceneCoords,
|
viewportCoordsToSceneCoords,
|
||||||
|
restoreElements,
|
||||||
} = window.ExcalidrawLib;
|
} = window.ExcalidrawLib;
|
||||||
|
|
||||||
const COMMENT_SVG = (
|
const COMMENT_SVG = (
|
||||||
|
@ -41,7 +78,7 @@ const COMMENT_SVG = (
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
class="feather feather-message-circle"
|
className="feather feather-message-circle"
|
||||||
>
|
>
|
||||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
|
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -50,18 +87,6 @@ const COMMENT_ICON_DIMENSION = 32;
|
||||||
const COMMENT_INPUT_HEIGHT = 50;
|
const COMMENT_INPUT_HEIGHT = 50;
|
||||||
const COMMENT_INPUT_WIDTH = 150;
|
const COMMENT_INPUT_WIDTH = 150;
|
||||||
|
|
||||||
const resolvablePromise = () => {
|
|
||||||
let resolve;
|
|
||||||
let reject;
|
|
||||||
const promise = new Promise((_resolve, _reject) => {
|
|
||||||
resolve = _resolve;
|
|
||||||
reject = _reject;
|
|
||||||
});
|
|
||||||
promise.resolve = resolve;
|
|
||||||
promise.reject = reject;
|
|
||||||
return promise;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderTopRightUI = () => {
|
const renderTopRightUI = () => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
@ -75,25 +100,31 @@ const renderTopRightUI = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const appRef = useRef(null);
|
const appRef = useRef<any>(null);
|
||||||
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
||||||
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
||||||
const [gridModeEnabled, setGridModeEnabled] = useState(false);
|
const [gridModeEnabled, setGridModeEnabled] = useState(false);
|
||||||
const [blobUrl, setBlobUrl] = useState(null);
|
const [blobUrl, setBlobUrl] = useState<string>("");
|
||||||
const [canvasUrl, setCanvasUrl] = useState(null);
|
const [canvasUrl, setCanvasUrl] = useState<string>("");
|
||||||
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
||||||
const [exportEmbedScene, setExportEmbedScene] = useState(false);
|
const [exportEmbedScene, setExportEmbedScene] = useState(false);
|
||||||
const [theme, setTheme] = useState("light");
|
const [theme, setTheme] = useState("light");
|
||||||
const [isCollaborating, setIsCollaborating] = useState(false);
|
const [isCollaborating, setIsCollaborating] = useState(false);
|
||||||
const [commentIcons, setCommentIcons] = useState({});
|
const [commentIcons, setCommentIcons] = useState<{ [id: string]: Comment }>(
|
||||||
const [comment, setComment] = useState(null);
|
{},
|
||||||
|
);
|
||||||
|
const [comment, setComment] = useState<Comment | null>(null);
|
||||||
|
|
||||||
const initialStatePromiseRef = useRef({ promise: null });
|
const initialStatePromiseRef = useRef<{
|
||||||
|
promise: ResolvablePromise<ExcalidrawInitialDataState | null>;
|
||||||
|
}>({ promise: null! });
|
||||||
if (!initialStatePromiseRef.current.promise) {
|
if (!initialStatePromiseRef.current.promise) {
|
||||||
initialStatePromiseRef.current.promise = resolvablePromise();
|
initialStatePromiseRef.current.promise =
|
||||||
|
resolvablePromise<ExcalidrawInitialDataState | null>();
|
||||||
}
|
}
|
||||||
|
|
||||||
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
|
const [excalidrawAPI, setExcalidrawAPI] =
|
||||||
|
useState<ExcalidrawImperativeAPI | null>(null);
|
||||||
|
|
||||||
useHandleLibrary({ excalidrawAPI });
|
useHandleLibrary({ excalidrawAPI });
|
||||||
|
|
||||||
|
@ -108,16 +139,17 @@ export default function App() {
|
||||||
reader.readAsDataURL(imageData);
|
reader.readAsDataURL(imageData);
|
||||||
|
|
||||||
reader.onload = function () {
|
reader.onload = function () {
|
||||||
const imagesArray = [
|
const imagesArray: BinaryFileData[] = [
|
||||||
{
|
{
|
||||||
id: "rocket",
|
id: "rocket" as BinaryFileData["id"],
|
||||||
dataURL: reader.result,
|
dataURL: reader.result as BinaryFileData["dataURL"],
|
||||||
mimeType: MIME_TYPES.jpg,
|
mimeType: MIME_TYPES.jpg,
|
||||||
created: 1644915140367,
|
created: 1644915140367,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
initialStatePromiseRef.current.promise.resolve(InitialData);
|
//@ts-ignore
|
||||||
|
initialStatePromiseRef.current.promise.resolve(initialData);
|
||||||
excalidrawAPI.addFiles(imagesArray);
|
excalidrawAPI.addFiles(imagesArray);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -131,7 +163,7 @@ export default function App() {
|
||||||
<button
|
<button
|
||||||
className="custom-element"
|
className="custom-element"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
excalidrawAPI.setActiveTool({
|
excalidrawAPI?.setActiveTool({
|
||||||
type: "custom",
|
type: "custom",
|
||||||
customType: "comment",
|
customType: "comment",
|
||||||
});
|
});
|
||||||
|
@ -151,7 +183,7 @@ export default function App() {
|
||||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
|
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
|
||||||
</svg>`,
|
</svg>`,
|
||||||
)}`;
|
)}`;
|
||||||
excalidrawAPI.setCursor(`url(${url}), auto`);
|
excalidrawAPI?.setCursor(`url(${url}), auto`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{COMMENT_SVG}
|
{COMMENT_SVG}
|
||||||
|
@ -168,10 +200,10 @@ export default function App() {
|
||||||
const file = await fileOpen({ description: "Excalidraw or library file" });
|
const file = await fileOpen({ description: "Excalidraw or library file" });
|
||||||
const contents = await loadSceneOrLibraryFromBlob(file, null, null);
|
const contents = await loadSceneOrLibraryFromBlob(file, null, null);
|
||||||
if (contents.type === MIME_TYPES.excalidraw) {
|
if (contents.type === MIME_TYPES.excalidraw) {
|
||||||
excalidrawAPI.updateScene(contents.data);
|
excalidrawAPI?.updateScene(contents.data as any);
|
||||||
} else if (contents.type === MIME_TYPES.excalidrawlib) {
|
} else if (contents.type === MIME_TYPES.excalidrawlib) {
|
||||||
excalidrawAPI.updateLibrary({
|
excalidrawAPI?.updateLibrary({
|
||||||
libraryItems: contents.data.libraryItems,
|
libraryItems: (contents.data as ImportedLibraryData).libraryItems!,
|
||||||
openLibraryMenu: true,
|
openLibraryMenu: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -179,34 +211,42 @@ export default function App() {
|
||||||
|
|
||||||
const updateScene = () => {
|
const updateScene = () => {
|
||||||
const sceneData = {
|
const sceneData = {
|
||||||
elements: [
|
elements: restoreElements(
|
||||||
{
|
[
|
||||||
type: "rectangle",
|
{
|
||||||
version: 141,
|
type: "rectangle",
|
||||||
versionNonce: 361174001,
|
version: 141,
|
||||||
isDeleted: false,
|
versionNonce: 361174001,
|
||||||
id: "oDVXy8D6rom3H1-LLH2-f",
|
isDeleted: false,
|
||||||
fillStyle: "hachure",
|
id: "oDVXy8D6rom3H1-LLH2-f",
|
||||||
strokeWidth: 1,
|
fillStyle: "hachure",
|
||||||
strokeStyle: "solid",
|
strokeWidth: 1,
|
||||||
roughness: 1,
|
strokeStyle: "solid",
|
||||||
opacity: 100,
|
roughness: 1,
|
||||||
angle: 0,
|
opacity: 100,
|
||||||
x: 100.50390625,
|
angle: 0,
|
||||||
y: 93.67578125,
|
x: 100.50390625,
|
||||||
strokeColor: "#c92a2a",
|
y: 93.67578125,
|
||||||
backgroundColor: "transparent",
|
strokeColor: "#c92a2a",
|
||||||
width: 186.47265625,
|
backgroundColor: "transparent",
|
||||||
height: 141.9765625,
|
width: 186.47265625,
|
||||||
seed: 1968410350,
|
height: 141.9765625,
|
||||||
groupIds: [],
|
seed: 1968410350,
|
||||||
},
|
groupIds: [],
|
||||||
],
|
boundElements: null,
|
||||||
|
locked: false,
|
||||||
|
link: null,
|
||||||
|
updated: 1,
|
||||||
|
strokeSharpness: "round",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
null,
|
||||||
|
),
|
||||||
appState: {
|
appState: {
|
||||||
viewBackgroundColor: "#edf2ff",
|
viewBackgroundColor: "#edf2ff",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
excalidrawAPI.updateScene(sceneData);
|
excalidrawAPI?.updateScene(sceneData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onLinkOpen = useCallback((element, event) => {
|
const onLinkOpen = useCallback((element, event) => {
|
||||||
|
@ -224,19 +264,26 @@ export default function App() {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onCopy = async (type) => {
|
const onCopy = async (type: string) => {
|
||||||
await exportToClipboard({
|
await exportToClipboard({
|
||||||
elements: excalidrawAPI.getSceneElements(),
|
elements: excalidrawAPI?.getSceneElements(),
|
||||||
appState: excalidrawAPI.getAppState(),
|
appState: excalidrawAPI?.getAppState(),
|
||||||
files: excalidrawAPI.getFiles(),
|
files: excalidrawAPI?.getFiles(),
|
||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
window.alert(`Copied to clipboard as ${type} sucessfully`);
|
window.alert(`Copied to clipboard as ${type} sucessfully`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [pointerData, setPointerData] = useState(null);
|
const [pointerData, setPointerData] = useState<{
|
||||||
|
pointer: { x: number; y: number };
|
||||||
|
button: "down" | "up";
|
||||||
|
pointersMap: Gesture["pointers"];
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
const onPointerDown = (activeTool, pointerDownState) => {
|
const onPointerDown = (
|
||||||
|
activeTool: AppState["activeTool"],
|
||||||
|
pointerDownState: ExcalidrawPointerDownState,
|
||||||
|
) => {
|
||||||
if (activeTool.type === "custom" && activeTool.customType === "comment") {
|
if (activeTool.type === "custom" && activeTool.customType === "comment") {
|
||||||
const { x, y } = pointerDownState.origin;
|
const { x, y } = pointerDownState.origin;
|
||||||
setComment({ x, y, value: "" });
|
setComment({ x, y, value: "" });
|
||||||
|
@ -244,48 +291,53 @@ export default function App() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const rerenderCommentIcons = () => {
|
const rerenderCommentIcons = () => {
|
||||||
const commentIconsElements =
|
const commentIconsElements = appRef.current.querySelectorAll(
|
||||||
appRef.current.querySelectorAll(".comment-icon");
|
".comment-icon",
|
||||||
|
) as HTMLElement[];
|
||||||
commentIconsElements.forEach((ele) => {
|
commentIconsElements.forEach((ele) => {
|
||||||
const id = ele.id;
|
const id = ele.id;
|
||||||
const appstate = excalidrawAPI.getAppState();
|
const appstate = excalidrawAPI?.getAppState();
|
||||||
const { x, y } = sceneCoordsToViewportCoords(
|
const { x, y } = sceneCoordsToViewportCoords(
|
||||||
{ sceneX: commentIcons[id].x, sceneY: commentIcons[id].y },
|
{ sceneX: commentIcons[id].x, sceneY: commentIcons[id].y },
|
||||||
appstate,
|
appstate,
|
||||||
);
|
);
|
||||||
ele.style.left = `${
|
ele.style.left = `${
|
||||||
x - COMMENT_ICON_DIMENSION / 2 - appstate.offsetLeft
|
x - COMMENT_ICON_DIMENSION / 2 - appstate!.offsetLeft
|
||||||
}px`;
|
}px`;
|
||||||
ele.style.top = `${
|
ele.style.top = `${
|
||||||
y - COMMENT_ICON_DIMENSION / 2 - appstate.offsetTop
|
y - COMMENT_ICON_DIMENSION / 2 - appstate!.offsetTop
|
||||||
}px`;
|
}px`;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPointerMoveFromPointerDownHandler = (pointerDownState) => {
|
const onPointerMoveFromPointerDownHandler = (
|
||||||
|
pointerDownState: PointerDownState,
|
||||||
|
) => {
|
||||||
return withBatchedUpdatesThrottled((event) => {
|
return withBatchedUpdatesThrottled((event) => {
|
||||||
const { x, y } = viewportCoordsToSceneCoords(
|
const { x, y } = viewportCoordsToSceneCoords(
|
||||||
{
|
{
|
||||||
clientX: event.clientX - pointerDownState.hitElementOffsets.x,
|
clientX: event.clientX - pointerDownState.hitElementOffsets.x,
|
||||||
clientY: event.clientY - pointerDownState.hitElementOffsets.y,
|
clientY: event.clientY - pointerDownState.hitElementOffsets.y,
|
||||||
},
|
},
|
||||||
excalidrawAPI.getAppState(),
|
excalidrawAPI?.getAppState(),
|
||||||
);
|
);
|
||||||
setCommentIcons({
|
setCommentIcons({
|
||||||
...commentIcons,
|
...commentIcons,
|
||||||
[pointerDownState.hitElement.id]: {
|
[pointerDownState.hitElement.id!]: {
|
||||||
...commentIcons[pointerDownState.hitElement.id],
|
...commentIcons[pointerDownState.hitElement.id!],
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const onPointerUpFromPointerDownHandler = (pointerDownState) => {
|
const onPointerUpFromPointerDownHandler = (
|
||||||
|
pointerDownState: PointerDownState,
|
||||||
|
) => {
|
||||||
return withBatchedUpdates((event) => {
|
return withBatchedUpdates((event) => {
|
||||||
window.removeEventListener(EVENT.POINTER_MOVE, pointerDownState.onMove);
|
window.removeEventListener(EVENT.POINTER_MOVE, pointerDownState.onMove);
|
||||||
window.removeEventListener(EVENT.POINTER_UP, pointerDownState.onUp);
|
window.removeEventListener(EVENT.POINTER_UP, pointerDownState.onUp);
|
||||||
excalidrawAPI.setActiveTool({ type: "selection" });
|
excalidrawAPI?.setActiveTool({ type: "selection" });
|
||||||
const distance = distance2d(
|
const distance = distance2d(
|
||||||
pointerDownState.x,
|
pointerDownState.x,
|
||||||
pointerDownState.y,
|
pointerDownState.y,
|
||||||
|
@ -308,18 +360,18 @@ export default function App() {
|
||||||
};
|
};
|
||||||
const renderCommentIcons = () => {
|
const renderCommentIcons = () => {
|
||||||
return Object.values(commentIcons).map((commentIcon) => {
|
return Object.values(commentIcons).map((commentIcon) => {
|
||||||
const appState = excalidrawAPI.getAppState();
|
const appState = excalidrawAPI?.getAppState();
|
||||||
const { x, y } = sceneCoordsToViewportCoords(
|
const { x, y } = sceneCoordsToViewportCoords(
|
||||||
{ sceneX: commentIcon.x, sceneY: commentIcon.y },
|
{ sceneX: commentIcon.x, sceneY: commentIcon.y },
|
||||||
excalidrawAPI.getAppState(),
|
excalidrawAPI?.getAppState(),
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id={commentIcon.id}
|
id={commentIcon.id}
|
||||||
key={commentIcon.id}
|
key={commentIcon.id}
|
||||||
style={{
|
style={{
|
||||||
top: `${y - COMMENT_ICON_DIMENSION / 2 - appState.offsetTop}px`,
|
top: `${y - COMMENT_ICON_DIMENSION / 2 - appState!.offsetTop}px`,
|
||||||
left: `${x - COMMENT_ICON_DIMENSION / 2 - appState.offsetLeft}px`,
|
left: `${x - COMMENT_ICON_DIMENSION / 2 - appState!.offsetLeft}px`,
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
width: `${COMMENT_ICON_DIMENSION}px`,
|
width: `${COMMENT_ICON_DIMENSION}px`,
|
||||||
|
@ -334,7 +386,7 @@ export default function App() {
|
||||||
commentIcon.value = comment.value;
|
commentIcon.value = comment.value;
|
||||||
saveComment();
|
saveComment();
|
||||||
}
|
}
|
||||||
const pointerDownState = {
|
const pointerDownState: any = {
|
||||||
x: event.clientX,
|
x: event.clientX,
|
||||||
y: event.clientY,
|
y: event.clientY,
|
||||||
hitElement: commentIcon,
|
hitElement: commentIcon,
|
||||||
|
@ -350,7 +402,7 @@ export default function App() {
|
||||||
pointerDownState.onMove = onPointerMove;
|
pointerDownState.onMove = onPointerMove;
|
||||||
pointerDownState.onUp = onPointerUp;
|
pointerDownState.onUp = onPointerUp;
|
||||||
|
|
||||||
excalidrawAPI.setActiveTool({
|
excalidrawAPI?.setActiveTool({
|
||||||
type: "custom",
|
type: "custom",
|
||||||
customType: "comment",
|
customType: "comment",
|
||||||
});
|
});
|
||||||
|
@ -365,6 +417,9 @@ export default function App() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveComment = () => {
|
const saveComment = () => {
|
||||||
|
if (!comment) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!comment.id && !comment.value) {
|
if (!comment.id && !comment.value) {
|
||||||
setComment(null);
|
setComment(null);
|
||||||
return;
|
return;
|
||||||
|
@ -383,7 +438,10 @@ export default function App() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderComment = () => {
|
const renderComment = () => {
|
||||||
const appState = excalidrawAPI.getAppState();
|
if (!comment) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const appState = excalidrawAPI?.getAppState()!;
|
||||||
const { x, y } = sceneCoordsToViewportCoords(
|
const { x, y } = sceneCoordsToViewportCoords(
|
||||||
{ sceneX: comment.x, sceneY: comment.y },
|
{ sceneX: comment.x, sceneY: comment.y },
|
||||||
appState,
|
appState,
|
||||||
|
@ -450,24 +508,29 @@ export default function App() {
|
||||||
<button
|
<button
|
||||||
className="reset-scene"
|
className="reset-scene"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
excalidrawAPI.resetScene();
|
excalidrawAPI?.resetScene();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reset Scene
|
Reset Scene
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
excalidrawAPI.updateLibrary({
|
const libraryItems: LibraryItems = [
|
||||||
libraryItems: [
|
{
|
||||||
{
|
status: "published",
|
||||||
status: "published",
|
id: "1",
|
||||||
elements: initialData.libraryItems[0],
|
created: 1,
|
||||||
},
|
elements: initialData.libraryItems[1] as any,
|
||||||
{
|
},
|
||||||
status: "unpublished",
|
{
|
||||||
elements: initialData.libraryItems[1],
|
status: "unpublished",
|
||||||
},
|
id: "2",
|
||||||
],
|
created: 2,
|
||||||
|
elements: initialData.libraryItems[1] as any,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
excalidrawAPI?.updateLibrary({
|
||||||
|
libraryItems,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -535,9 +598,9 @@ export default function App() {
|
||||||
username: "fallback",
|
username: "fallback",
|
||||||
avatarUrl: "https://example.com",
|
avatarUrl: "https://example.com",
|
||||||
});
|
});
|
||||||
excalidrawAPI.updateScene({ collaborators });
|
excalidrawAPI?.updateScene({ collaborators });
|
||||||
} else {
|
} else {
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI?.updateScene({
|
||||||
collaborators: new Map(),
|
collaborators: new Map(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -571,12 +634,16 @@ export default function App() {
|
||||||
</div>
|
</div>
|
||||||
<div className="excalidraw-wrapper">
|
<div className="excalidraw-wrapper">
|
||||||
<Excalidraw
|
<Excalidraw
|
||||||
ref={(api) => setExcalidrawAPI(api)}
|
ref={(api: ExcalidrawImperativeAPI) => setExcalidrawAPI(api)}
|
||||||
initialData={initialStatePromiseRef.current.promise}
|
initialData={initialStatePromiseRef.current.promise}
|
||||||
onChange={(elements, state) => {
|
onChange={(elements: ExcalidrawElement[], state: AppState) => {
|
||||||
console.info("Elements :", elements, "State : ", state);
|
console.info("Elements :", elements, "State : ", state);
|
||||||
}}
|
}}
|
||||||
onPointerUpdate={(payload) => setPointerData(payload)}
|
onPointerUpdate={(payload: {
|
||||||
|
pointer: { x: number; y: number };
|
||||||
|
button: "down" | "up";
|
||||||
|
pointersMap: Gesture["pointers"];
|
||||||
|
}) => setPointerData(payload)}
|
||||||
onCollabButtonClick={() =>
|
onCollabButtonClick={() =>
|
||||||
window.alert("You clicked on collab button")
|
window.alert("You clicked on collab button")
|
||||||
}
|
}
|
||||||
|
@ -616,7 +683,7 @@ export default function App() {
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const svg = await exportToSvg({
|
const svg = await exportToSvg({
|
||||||
elements: excalidrawAPI.getSceneElements(),
|
elements: excalidrawAPI?.getSceneElements(),
|
||||||
appState: {
|
appState: {
|
||||||
...initialData.appState,
|
...initialData.appState,
|
||||||
exportWithDarkMode,
|
exportWithDarkMode,
|
||||||
|
@ -625,7 +692,7 @@ export default function App() {
|
||||||
height: 100,
|
height: 100,
|
||||||
},
|
},
|
||||||
embedScene: true,
|
embedScene: true,
|
||||||
files: excalidrawAPI.getFiles(),
|
files: excalidrawAPI?.getFiles(),
|
||||||
});
|
});
|
||||||
appRef.current.querySelector(".export-svg").innerHTML =
|
appRef.current.querySelector(".export-svg").innerHTML =
|
||||||
svg.outerHTML;
|
svg.outerHTML;
|
||||||
|
@ -638,14 +705,14 @@ export default function App() {
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const blob = await exportToBlob({
|
const blob = await exportToBlob({
|
||||||
elements: excalidrawAPI.getSceneElements(),
|
elements: excalidrawAPI?.getSceneElements(),
|
||||||
mimeType: "image/png",
|
mimeType: "image/png",
|
||||||
appState: {
|
appState: {
|
||||||
...initialData.appState,
|
...initialData.appState,
|
||||||
exportEmbedScene,
|
exportEmbedScene,
|
||||||
exportWithDarkMode,
|
exportWithDarkMode,
|
||||||
},
|
},
|
||||||
files: excalidrawAPI.getFiles(),
|
files: excalidrawAPI?.getFiles(),
|
||||||
});
|
});
|
||||||
setBlobUrl(window.URL.createObjectURL(blob));
|
setBlobUrl(window.URL.createObjectURL(blob));
|
||||||
}}
|
}}
|
||||||
|
@ -659,12 +726,12 @@ export default function App() {
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const canvas = await exportToCanvas({
|
const canvas = await exportToCanvas({
|
||||||
elements: excalidrawAPI.getSceneElements(),
|
elements: excalidrawAPI?.getSceneElements(),
|
||||||
appState: {
|
appState: {
|
||||||
...initialData.appState,
|
...initialData.appState,
|
||||||
exportWithDarkMode,
|
exportWithDarkMode,
|
||||||
},
|
},
|
||||||
files: excalidrawAPI.getFiles(),
|
files: excalidrawAPI?.getFiles(),
|
||||||
});
|
});
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
ctx.font = "30px Virgil";
|
ctx.font = "30px Virgil";
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import "./Sidebar.scss";
|
import "./Sidebar.scss";
|
||||||
export default function Sidebar(props) {
|
export default function Sidebar({ children }: { children: React.ReactNode }) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -23,7 +23,7 @@ export default function Sidebar(props) {
|
||||||
>
|
>
|
||||||
Open Sidebar
|
Open Sidebar
|
||||||
</button>
|
</button>
|
||||||
{props.children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
|
@ -44,6 +44,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||||
const canvasActions = props.UIOptions?.canvasActions;
|
const canvasActions = props.UIOptions?.canvasActions;
|
||||||
|
|
||||||
const UIOptions: AppProps["UIOptions"] = {
|
const UIOptions: AppProps["UIOptions"] = {
|
||||||
|
...props.UIOptions,
|
||||||
canvasActions: {
|
canvasActions: {
|
||||||
...DEFAULT_UI_OPTIONS.canvasActions,
|
...DEFAULT_UI_OPTIONS.canvasActions,
|
||||||
...canvasActions,
|
...canvasActions,
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
"@babel/plugin-transform-async-to-generator": "7.16.0",
|
"@babel/plugin-transform-async-to-generator": "7.16.0",
|
||||||
"@babel/plugin-transform-runtime": "7.17.10",
|
"@babel/plugin-transform-runtime": "7.17.10",
|
||||||
"@babel/plugin-transform-typescript": "7.16.1",
|
"@babel/plugin-transform-typescript": "7.16.1",
|
||||||
"@babel/preset-env": "7.17.10",
|
"@babel/preset-env": "7.18.6",
|
||||||
"@babel/preset-react": "7.16.7",
|
"@babel/preset-react": "7.16.7",
|
||||||
"@babel/preset-typescript": "7.16.7",
|
"@babel/preset-typescript": "7.16.7",
|
||||||
"autoprefixer": "10.4.7",
|
"autoprefixer": "10.4.7",
|
||||||
|
@ -66,7 +66,7 @@
|
||||||
"webpack": "5.72.0",
|
"webpack": "5.72.0",
|
||||||
"webpack-bundle-analyzer": "4.5.0",
|
"webpack-bundle-analyzer": "4.5.0",
|
||||||
"webpack-cli": "4.9.2",
|
"webpack-cli": "4.9.2",
|
||||||
"webpack-dev-server": "4.9.0",
|
"webpack-dev-server": "4.9.3",
|
||||||
"webpack-merge": "5.8.0"
|
"webpack-merge": "5.8.0"
|
||||||
},
|
},
|
||||||
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||||
|
|
|
@ -5,7 +5,7 @@ const devConfig = require("./webpack.dev.config");
|
||||||
|
|
||||||
const devServerConfig = {
|
const devServerConfig = {
|
||||||
entry: {
|
entry: {
|
||||||
bundle: "./example/index.js",
|
bundle: "./example/index.tsx",
|
||||||
},
|
},
|
||||||
// Server Configuration options
|
// Server Configuration options
|
||||||
devServer: {
|
devServer: {
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -37,7 +37,7 @@
|
||||||
"@babel/core": "7.17.2",
|
"@babel/core": "7.17.2",
|
||||||
"@babel/plugin-transform-arrow-functions": "7.16.0",
|
"@babel/plugin-transform-arrow-functions": "7.16.0",
|
||||||
"@babel/plugin-transform-async-to-generator": "7.16.5",
|
"@babel/plugin-transform-async-to-generator": "7.16.5",
|
||||||
"@babel/plugin-transform-runtime": "7.17.10",
|
"@babel/plugin-transform-runtime": "7.18.6",
|
||||||
"@babel/plugin-transform-typescript": "7.16.1",
|
"@babel/plugin-transform-typescript": "7.16.1",
|
||||||
"@babel/preset-env": "7.16.7",
|
"@babel/preset-env": "7.16.7",
|
||||||
"@babel/preset-typescript": "7.16.7",
|
"@babel/preset-typescript": "7.16.7",
|
||||||
|
|
|
@ -205,12 +205,12 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "^7.16.7"
|
"@babel/types" "^7.16.7"
|
||||||
|
|
||||||
"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.0", "@babel/helper-module-imports@^7.16.7":
|
"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.0", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.18.6":
|
||||||
version "7.16.7"
|
version "7.18.6"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437"
|
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e"
|
||||||
integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==
|
integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "^7.16.7"
|
"@babel/types" "^7.18.6"
|
||||||
|
|
||||||
"@babel/helper-module-transforms@^7.16.7":
|
"@babel/helper-module-transforms@^7.16.7":
|
||||||
version "7.16.7"
|
version "7.16.7"
|
||||||
|
@ -233,10 +233,10 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "^7.16.7"
|
"@babel/types" "^7.16.7"
|
||||||
|
|
||||||
"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
|
"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
|
||||||
version "7.16.7"
|
version "7.18.6"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz#aa3a8ab4c3cceff8e65eb9e73d87dc4ff320b2f5"
|
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.6.tgz#9448974dd4fb1d80fefe72e8a0af37809cd30d6d"
|
||||||
integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==
|
integrity sha512-gvZnm1YAAxh13eJdkb9EWHBnF3eAub3XTLCZEehHT2kWxiKVRL64+ae5Y6Ivne0mVHmMYKT+xWgZO+gQhuLUBg==
|
||||||
|
|
||||||
"@babel/helper-remap-async-to-generator@^7.16.5":
|
"@babel/helper-remap-async-to-generator@^7.16.5":
|
||||||
version "7.16.5"
|
version "7.16.5"
|
||||||
|
@ -293,6 +293,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad"
|
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad"
|
||||||
integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==
|
integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier@^7.18.6":
|
||||||
|
version "7.18.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076"
|
||||||
|
integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==
|
||||||
|
|
||||||
"@babel/helper-validator-option@^7.16.7":
|
"@babel/helper-validator-option@^7.16.7":
|
||||||
version "7.16.7"
|
version "7.16.7"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23"
|
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23"
|
||||||
|
@ -805,16 +810,16 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-plugin-utils" "^7.16.7"
|
"@babel/helper-plugin-utils" "^7.16.7"
|
||||||
|
|
||||||
"@babel/plugin-transform-runtime@7.17.10":
|
"@babel/plugin-transform-runtime@7.18.6":
|
||||||
version "7.17.10"
|
version "7.18.6"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.17.10.tgz#b89d821c55d61b5e3d3c3d1d636d8d5a81040ae1"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.18.6.tgz#77b14416015ea93367ca06979710f5000ff34ccb"
|
||||||
integrity sha512-6jrMilUAJhktTr56kACL8LnWC5hx3Lf27BS0R0DSyW/OoJfb/iTHeE96V3b1dgKG3FSFdd/0culnYWMkjcKCig==
|
integrity sha512-8uRHk9ZmRSnWqUgyae249EJZ94b0yAGLBIqzZzl+0iEdbno55Pmlt/32JZsHwXD9k/uZj18Aqqk35wBX4CBTXA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-module-imports" "^7.16.7"
|
"@babel/helper-module-imports" "^7.18.6"
|
||||||
"@babel/helper-plugin-utils" "^7.16.7"
|
"@babel/helper-plugin-utils" "^7.18.6"
|
||||||
babel-plugin-polyfill-corejs2 "^0.3.0"
|
babel-plugin-polyfill-corejs2 "^0.3.1"
|
||||||
babel-plugin-polyfill-corejs3 "^0.5.0"
|
babel-plugin-polyfill-corejs3 "^0.5.2"
|
||||||
babel-plugin-polyfill-regenerator "^0.3.0"
|
babel-plugin-polyfill-regenerator "^0.3.1"
|
||||||
semver "^6.3.0"
|
semver "^6.3.0"
|
||||||
|
|
||||||
"@babel/plugin-transform-shorthand-properties@^7.16.7":
|
"@babel/plugin-transform-shorthand-properties@^7.16.7":
|
||||||
|
@ -1026,6 +1031,14 @@
|
||||||
"@babel/helper-validator-identifier" "^7.16.7"
|
"@babel/helper-validator-identifier" "^7.16.7"
|
||||||
to-fast-properties "^2.0.0"
|
to-fast-properties "^2.0.0"
|
||||||
|
|
||||||
|
"@babel/types@^7.18.6":
|
||||||
|
version "7.18.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.7.tgz#a4a2c910c15040ea52cdd1ddb1614a65c8041726"
|
||||||
|
integrity sha512-QG3yxTcTIBoAcQmkCs+wAPYZhu7Dk9rXKacINfNbdJDNERTbLQbHGyVG8q/YGMPeCJRIhSY0+fTc5+xuh6WPSQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-validator-identifier" "^7.18.6"
|
||||||
|
to-fast-properties "^2.0.0"
|
||||||
|
|
||||||
"@discoveryjs/json-ext@^0.5.0":
|
"@discoveryjs/json-ext@^0.5.0":
|
||||||
version "0.5.2"
|
version "0.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.2.tgz#8f03a22a04de437254e8ce8cc84ba39689288752"
|
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.2.tgz#8f03a22a04de437254e8ce8cc84ba39689288752"
|
||||||
|
@ -1344,13 +1357,13 @@ babel-plugin-dynamic-import-node@^2.3.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
object.assign "^4.1.0"
|
object.assign "^4.1.0"
|
||||||
|
|
||||||
babel-plugin-polyfill-corejs2@^0.3.0:
|
babel-plugin-polyfill-corejs2@^0.3.0, babel-plugin-polyfill-corejs2@^0.3.1:
|
||||||
version "0.3.0"
|
version "0.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.0.tgz#407082d0d355ba565af24126fb6cb8e9115251fd"
|
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz#440f1b70ccfaabc6b676d196239b138f8a2cfba5"
|
||||||
integrity sha512-wMDoBJ6uG4u4PNFh72Ty6t3EgfA91puCuAwKIazbQlci+ENb/UU9A3xG5lutjUIiXCIn1CY5L15r9LimiJyrSA==
|
integrity sha512-v7/T6EQcNfVLfcN2X8Lulb7DjprieyLWJK/zOWH5DUYcAgex9sP3h25Q+DLsX9TloXe3y1O8l2q2Jv9q8UVB9w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/compat-data" "^7.13.11"
|
"@babel/compat-data" "^7.13.11"
|
||||||
"@babel/helper-define-polyfill-provider" "^0.3.0"
|
"@babel/helper-define-polyfill-provider" "^0.3.1"
|
||||||
semver "^6.1.1"
|
semver "^6.1.1"
|
||||||
|
|
||||||
babel-plugin-polyfill-corejs3@^0.4.0:
|
babel-plugin-polyfill-corejs3@^0.4.0:
|
||||||
|
@ -1361,7 +1374,7 @@ babel-plugin-polyfill-corejs3@^0.4.0:
|
||||||
"@babel/helper-define-polyfill-provider" "^0.3.0"
|
"@babel/helper-define-polyfill-provider" "^0.3.0"
|
||||||
core-js-compat "^3.18.0"
|
core-js-compat "^3.18.0"
|
||||||
|
|
||||||
babel-plugin-polyfill-corejs3@^0.5.0:
|
babel-plugin-polyfill-corejs3@^0.5.2:
|
||||||
version "0.5.2"
|
version "0.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz#aabe4b2fa04a6e038b688c5e55d44e78cd3a5f72"
|
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz#aabe4b2fa04a6e038b688c5e55d44e78cd3a5f72"
|
||||||
integrity sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ==
|
integrity sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ==
|
||||||
|
@ -1369,12 +1382,12 @@ babel-plugin-polyfill-corejs3@^0.5.0:
|
||||||
"@babel/helper-define-polyfill-provider" "^0.3.1"
|
"@babel/helper-define-polyfill-provider" "^0.3.1"
|
||||||
core-js-compat "^3.21.0"
|
core-js-compat "^3.21.0"
|
||||||
|
|
||||||
babel-plugin-polyfill-regenerator@^0.3.0:
|
babel-plugin-polyfill-regenerator@^0.3.0, babel-plugin-polyfill-regenerator@^0.3.1:
|
||||||
version "0.3.0"
|
version "0.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.0.tgz#9ebbcd7186e1a33e21c5e20cae4e7983949533be"
|
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz#2c0678ea47c75c8cc2fbb1852278d8fb68233990"
|
||||||
integrity sha512-dhAPTDLGoMW5/84wkgwiLRwMnio2i1fUe53EuvtKMv0pn2p3S8OCoV1xAzfJPl0KOX7IB89s2ib85vbYiea3jg==
|
integrity sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-define-polyfill-provider" "^0.3.0"
|
"@babel/helper-define-polyfill-provider" "^0.3.1"
|
||||||
|
|
||||||
babel-plugin-syntax-class-properties@^6.8.0:
|
babel-plugin-syntax-class-properties@^6.8.0:
|
||||||
version "6.13.0"
|
version "6.13.0"
|
||||||
|
|
|
@ -26,7 +26,11 @@ type Config = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const register = (config?: Config) => {
|
export const register = (config?: Config) => {
|
||||||
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
|
if (
|
||||||
|
(process.env.NODE_ENV === "production" ||
|
||||||
|
process.env.REACT_APP_DEV_ENABLE_SW?.toLowerCase() === "true") &&
|
||||||
|
"serviceWorker" in navigator
|
||||||
|
) {
|
||||||
// The URL constructor is available in all browsers that support SW.
|
// The URL constructor is available in all browsers that support SW.
|
||||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||||
if (publicUrl.origin !== window.location.origin) {
|
if (publicUrl.origin !== window.location.origin) {
|
||||||
|
|
|
@ -38,6 +38,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -55,7 +56,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -211,6 +212,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -228,7 +230,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -388,6 +390,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -405,7 +408,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -726,6 +729,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -743,7 +747,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -1064,6 +1068,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -1081,7 +1086,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -1241,6 +1246,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -1258,7 +1264,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -1454,6 +1460,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -1471,7 +1478,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -1726,6 +1733,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -1743,7 +1751,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
|
@ -2082,6 +2090,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -2099,7 +2108,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -2882,6 +2891,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -2899,7 +2909,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -3220,6 +3230,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -3237,7 +3248,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -3558,6 +3569,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -3575,7 +3587,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
|
@ -3976,6 +3988,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -3993,7 +4006,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id2": true,
|
"id2": true,
|
||||||
|
@ -4254,6 +4267,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -4271,7 +4285,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id2": true,
|
"id2": true,
|
||||||
|
@ -4613,6 +4627,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -4630,7 +4645,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -4719,6 +4734,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -4736,7 +4752,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -4803,6 +4819,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 100,
|
"height": 100,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -4820,7 +4837,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
|
|
@ -38,6 +38,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -55,7 +56,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
|
@ -547,6 +548,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -564,7 +566,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
|
@ -1062,6 +1064,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": false,
|
"isBindingEnabled": false,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -1079,7 +1082,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -1922,6 +1925,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -1939,7 +1943,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
|
@ -2143,6 +2147,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -2160,7 +2165,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id3": true,
|
"id3": true,
|
||||||
|
@ -2649,6 +2654,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -2666,7 +2672,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
|
@ -2925,6 +2931,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -2942,7 +2949,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -3102,6 +3109,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -3119,7 +3127,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id2": true,
|
"id2": true,
|
||||||
},
|
},
|
||||||
|
@ -3591,6 +3599,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -3608,7 +3617,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -3848,6 +3857,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -3865,7 +3875,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
|
@ -4069,6 +4079,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -4086,7 +4097,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
|
@ -4334,6 +4345,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -4351,7 +4363,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
|
@ -4609,6 +4621,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -4626,7 +4639,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id2": true,
|
"id2": true,
|
||||||
},
|
},
|
||||||
|
@ -5031,6 +5044,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -5048,7 +5062,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
|
@ -5355,6 +5369,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -5372,7 +5387,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
|
@ -5654,6 +5669,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -5671,7 +5687,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
|
@ -5881,6 +5897,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -5898,7 +5915,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
|
@ -6058,6 +6075,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -6075,7 +6093,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -6559,6 +6577,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -6576,7 +6595,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
|
@ -6907,6 +6926,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -6924,7 +6944,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -9146,6 +9166,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -9163,7 +9184,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id2": true,
|
"id2": true,
|
||||||
|
@ -9544,6 +9565,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -9561,7 +9583,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id2": true,
|
"id2": true,
|
||||||
|
@ -9820,6 +9842,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -9837,7 +9860,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id2": true,
|
"id2": true,
|
||||||
|
@ -10057,6 +10080,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -10074,7 +10098,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id2": true,
|
"id2": true,
|
||||||
|
@ -10363,6 +10387,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -10380,7 +10405,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -10540,6 +10565,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -10557,7 +10583,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -10717,6 +10743,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -10734,7 +10761,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -10894,6 +10921,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -10911,7 +10939,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -11101,6 +11129,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -11118,7 +11147,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -11308,6 +11337,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -11325,7 +11355,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -11533,6 +11563,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -11550,7 +11581,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -11740,6 +11771,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -11757,7 +11789,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -11917,6 +11949,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -11934,7 +11967,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -12124,6 +12157,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -12141,7 +12175,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -12301,6 +12335,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -12318,7 +12353,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -12478,6 +12513,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -12495,7 +12531,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -12703,6 +12739,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -12720,7 +12757,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
|
@ -13497,6 +13534,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -13514,7 +13552,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id3": true,
|
"id3": true,
|
||||||
|
@ -13773,6 +13811,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -13790,7 +13829,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": -2.916666666666668,
|
"scrollX": -2.916666666666668,
|
||||||
|
@ -13881,6 +13920,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -13898,7 +13938,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -13987,6 +14027,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -14004,7 +14045,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
|
@ -14167,6 +14208,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -14184,7 +14226,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
|
@ -14518,6 +14560,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -14535,7 +14578,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -14735,6 +14778,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -14752,7 +14796,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
|
@ -15647,6 +15691,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -15664,7 +15709,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 60,
|
"scrollX": 60,
|
||||||
|
@ -15753,6 +15798,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -15770,7 +15816,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
|
@ -16592,6 +16638,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -16609,7 +16656,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
"id2": true,
|
"id2": true,
|
||||||
|
@ -17039,6 +17086,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -17056,7 +17104,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {
|
"previousSelectedElementIds": Object {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
|
@ -17338,6 +17386,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -17355,7 +17404,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 10,
|
"scrollX": 10,
|
||||||
|
@ -17446,6 +17495,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -17463,7 +17513,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -17990,6 +18040,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -18007,7 +18058,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
@ -18096,6 +18147,7 @@ Object {
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"height": 768,
|
"height": 768,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -18113,7 +18165,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
|
|
@ -50,6 +50,7 @@ jest.mock("socket.io-client", () => {
|
||||||
return {
|
return {
|
||||||
close: () => {},
|
close: () => {},
|
||||||
on: () => {},
|
on: () => {},
|
||||||
|
once: () => {},
|
||||||
off: () => {},
|
off: () => {},
|
||||||
emit: () => {},
|
emit: () => {},
|
||||||
};
|
};
|
||||||
|
@ -77,7 +78,7 @@ describe("collaboration", () => {
|
||||||
]);
|
]);
|
||||||
expect(API.getStateHistory().length).toBe(1);
|
expect(API.getStateHistory().length).toBe(1);
|
||||||
});
|
});
|
||||||
window.collab.openPortal();
|
window.collab.startCollaboration(null);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]);
|
expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]);
|
||||||
expect(API.getStateHistory().length).toBe(1);
|
expect(API.getStateHistory().length).toBe(1);
|
||||||
|
|
|
@ -289,7 +289,7 @@ describe("contextMenu element", () => {
|
||||||
expect(copiedStyles).toBe("{}");
|
expect(copiedStyles).toBe("{}");
|
||||||
fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!);
|
fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!);
|
||||||
expect(copiedStyles).not.toBe("{}");
|
expect(copiedStyles).not.toBe("{}");
|
||||||
const element = JSON.parse(copiedStyles);
|
const element = JSON.parse(copiedStyles)[0];
|
||||||
expect(element).toEqual(API.getSelectedElement());
|
expect(element).toEqual(API.getSelectedElement());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -329,7 +329,7 @@ describe("contextMenu element", () => {
|
||||||
});
|
});
|
||||||
let contextMenu = UI.queryContextMenu();
|
let contextMenu = UI.queryContextMenu();
|
||||||
fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!);
|
fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!);
|
||||||
const secondRect = JSON.parse(copiedStyles);
|
const secondRect = JSON.parse(copiedStyles)[0];
|
||||||
expect(secondRect.id).toBe(h.elements[1].id);
|
expect(secondRect.id).toBe(h.elements[1].id);
|
||||||
|
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
|
|
|
@ -38,6 +38,7 @@ Object {
|
||||||
"fileHandle": null,
|
"fileHandle": null,
|
||||||
"gridSize": null,
|
"gridSize": null,
|
||||||
"isBindingEnabled": true,
|
"isBindingEnabled": true,
|
||||||
|
"isLibraryMenuDocked": false,
|
||||||
"isLibraryOpen": false,
|
"isLibraryOpen": false,
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
|
@ -53,7 +54,7 @@ Object {
|
||||||
},
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElement": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": Object {},
|
"previousSelectedElementIds": Object {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
|
|
43
src/types.ts
43
src/types.ts
|
@ -48,6 +48,8 @@ export type Collaborator = {
|
||||||
// The url of the collaborator's avatar, defaults to username intials
|
// The url of the collaborator's avatar, defaults to username intials
|
||||||
// if not present
|
// if not present
|
||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
|
// user id. If supplied, we'll filter out duplicates when rendering user avatars.
|
||||||
|
id?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DataURL = string & { _brand: "DataURL" };
|
export type DataURL = string & { _brand: "DataURL" };
|
||||||
|
@ -160,6 +162,7 @@ export type AppState = {
|
||||||
offsetLeft: number;
|
offsetLeft: number;
|
||||||
|
|
||||||
isLibraryOpen: boolean;
|
isLibraryOpen: boolean;
|
||||||
|
isLibraryMenuDocked: boolean;
|
||||||
fileHandle: FileSystemHandle | null;
|
fileHandle: FileSystemHandle | null;
|
||||||
collaborators: Map<string, Collaborator>;
|
collaborators: Map<string, Collaborator>;
|
||||||
showStats: boolean;
|
showStats: boolean;
|
||||||
|
@ -174,7 +177,7 @@ export type AppState = {
|
||||||
data: Spreadsheet;
|
data: Spreadsheet;
|
||||||
};
|
};
|
||||||
/** imageElement waiting to be placed on canvas */
|
/** imageElement waiting to be placed on canvas */
|
||||||
pendingImageElement: NonDeleted<ExcalidrawImageElement> | null;
|
pendingImageElementId: ExcalidrawImageElement["id"] | null;
|
||||||
showHyperlinkPopup: false | "info" | "editor";
|
showHyperlinkPopup: false | "info" | "editor";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -289,7 +292,10 @@ export interface ExcalidrawProps {
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => JSX.Element;
|
) => JSX.Element;
|
||||||
UIOptions?: UIOptions;
|
UIOptions?: {
|
||||||
|
dockedSidebarBreakpoint?: number;
|
||||||
|
canvasActions?: CanvasActions;
|
||||||
|
};
|
||||||
detectScroll?: boolean;
|
detectScroll?: boolean;
|
||||||
handleKeyboardGlobally?: boolean;
|
handleKeyboardGlobally?: boolean;
|
||||||
onLibraryChange?: (libraryItems: LibraryItems) => void | Promise<any>;
|
onLibraryChange?: (libraryItems: LibraryItems) => void | Promise<any>;
|
||||||
|
@ -347,18 +353,18 @@ type CanvasActions = {
|
||||||
saveAsImage?: boolean;
|
saveAsImage?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UIOptions = {
|
export type AppProps = Merge<
|
||||||
canvasActions?: CanvasActions;
|
ExcalidrawProps,
|
||||||
};
|
{
|
||||||
|
UIOptions: {
|
||||||
export type AppProps = ExcalidrawProps & {
|
canvasActions: Required<CanvasActions> & { export: ExportOpts };
|
||||||
UIOptions: {
|
dockedSidebarBreakpoint?: number;
|
||||||
canvasActions: Required<CanvasActions> & { export: ExportOpts };
|
};
|
||||||
};
|
detectScroll: boolean;
|
||||||
detectScroll: boolean;
|
handleKeyboardGlobally: boolean;
|
||||||
handleKeyboardGlobally: boolean;
|
isCollaborating: boolean;
|
||||||
isCollaborating: boolean;
|
}
|
||||||
};
|
>;
|
||||||
|
|
||||||
/** A subset of App class properties that we need to use elsewhere
|
/** A subset of App class properties that we need to use elsewhere
|
||||||
* in the app, eg Manager. Factored out into a separate type to keep DRY. */
|
* in the app, eg Manager. Factored out into a separate type to keep DRY. */
|
||||||
|
@ -375,7 +381,8 @@ export type AppClassProperties = {
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
deviceType: App["deviceType"];
|
device: App["device"];
|
||||||
|
scene: App["scene"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PointerDownState = Readonly<{
|
export type PointerDownState = Readonly<{
|
||||||
|
@ -470,7 +477,9 @@ export type ExcalidrawImperativeAPI = {
|
||||||
resetCursor: InstanceType<typeof App>["resetCursor"];
|
resetCursor: InstanceType<typeof App>["resetCursor"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeviceType = {
|
export type Device = Readonly<{
|
||||||
|
isSmScreen: boolean;
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
isTouchScreen: boolean;
|
isTouchScreen: boolean;
|
||||||
};
|
canDeviceFitSidebar: boolean;
|
||||||
|
}>;
|
||||||
|
|
13
src/utils.ts
13
src/utils.ts
|
@ -668,3 +668,16 @@ export const isPromiseLike = (
|
||||||
"finally" in value
|
"finally" in value
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const queryFocusableElements = (container: HTMLElement | null) => {
|
||||||
|
const focusableElements = container?.querySelectorAll<HTMLElement>(
|
||||||
|
"button, a, input, select, textarea, div[tabindex], label[tabindex]",
|
||||||
|
);
|
||||||
|
|
||||||
|
return focusableElements
|
||||||
|
? Array.from(focusableElements).filter(
|
||||||
|
(element) =>
|
||||||
|
element.tabIndex > -1 && !(element as HTMLInputElement).disabled,
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
};
|
||||||
|
|
10
vercel.json
10
vercel.json
|
@ -27,6 +27,16 @@
|
||||||
{
|
{
|
||||||
"source": "/webex/:match*",
|
"source": "/webex/:match*",
|
||||||
"destination": "https://for-webex.excalidraw.com"
|
"destination": "https://for-webex.excalidraw.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/:path*",
|
||||||
|
"has": [
|
||||||
|
{
|
||||||
|
"type": "host",
|
||||||
|
"value": "vscode.excalidraw.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"destination": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue