Compare commits

..

4 Commits

Author SHA1 Message Date
dwelle 1d65536360 improve logger 2022-08-20 18:01:26 +02:00
dwelle d731a6463c move debug helpers to app.tsx 2022-08-20 13:58:08 +02:00
dwelle 9f325a626e support serving local production builds 2022-08-20 12:47:12 +02:00
dwelle 41200ea28d [PERF DEBUG] 2022-08-20 11:57:36 +02:00
27 changed files with 539 additions and 397 deletions
+5 -2
View File
@@ -31,8 +31,10 @@
"@types/socket.io-client": "1.4.36", "@types/socket.io-client": "1.4.36",
"browser-fs-access": "0.29.1", "browser-fs-access": "0.29.1",
"clsx": "1.1.1", "clsx": "1.1.1",
"cross-env": "7.0.3",
"fake-indexeddb": "3.1.7", "fake-indexeddb": "3.1.7",
"firebase": "8.3.3", "firebase": "8.3.3",
"http-server": "14.1.1",
"i18next-browser-languagedetector": "6.1.4", "i18next-browser-languagedetector": "6.1.4",
"idb-keyval": "6.0.3", "idb-keyval": "6.0.3",
"image-blob-reduce": "3.0.1", "image-blob-reduce": "3.0.1",
@@ -91,8 +93,8 @@
"private": true, "private": true,
"scripts": { "scripts": {
"build-node": "node ./scripts/build-node.js", "build-node": "node ./scripts/build-node.js",
"build:app:docker": "REACT_APP_DISABLE_SENTRY=true react-scripts build", "build:app:docker": "cross-env REACT_APP_DISABLE_SENTRY=true react-scripts build",
"build:app": "REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts build", "build:app": "cross-env 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:prebuild": "node ./scripts/prebuild.js", "build:prebuild": "node ./scripts/prebuild.js",
"build": "yarn build:prebuild && yarn build:app && yarn build:version", "build": "yarn build:prebuild && yarn build:app && yarn build:version",
@@ -105,6 +107,7 @@
"prepare": "husky install", "prepare": "husky install",
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore", "prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
"start": "react-scripts start", "start": "react-scripts start",
"start:build": "npm run build && npx http-server build -a localhost -p 3001 -o",
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watchAll=false", "test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watchAll=false",
"test:app": "react-scripts test --passWithNoTests", "test:app": "react-scripts test --passWithNoTests",
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .", "test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
+3 -1
View File
@@ -18,4 +18,6 @@ const moveServiceWorkerScript = () => {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
moveServiceWorkerScript(); if (process.env.CI) {
moveServiceWorkerScript();
}
+1 -1
View File
@@ -244,7 +244,7 @@ export const actionLoadScene = register({
} }
}, },
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O, keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O,
PanelComponent: ({ updateData }) => ( PanelComponent: ({ updateData, appState }) => (
<ToolButton <ToolButton
type="button" type="button"
icon={load} icon={load}
-1
View File
@@ -147,7 +147,6 @@ export class ActionManager {
) { ) {
const action = this.actions[name]; const action = this.actions[name];
const PanelComponent = action.PanelComponent!; const PanelComponent = action.PanelComponent!;
PanelComponent.displayName = "PanelComponent";
const elements = this.getElementsIncludingDeleted(); const elements = this.getElementsIncludingDeleted();
const appState = this.getAppState(); const appState = this.getAppState();
const updateData = (formState?: any) => { const updateData = (formState?: any) => {
+12 -54
View File
@@ -26,17 +26,17 @@ import { ToolButton } from "./ToolButton";
import { hasStrokeColor } from "../scene/comparisons"; import { hasStrokeColor } from "../scene/comparisons";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { hasBoundTextElement, isBoundToContainer } from "../element/typeChecks"; import { hasBoundTextElement, isBoundToContainer } from "../element/typeChecks";
import clsx from "clsx";
import { actionToggleZenMode } from "../actions";
export const SelectedShapeActions = ({ export const SelectedShapeActions = ({
appState, appState,
elements, elements,
renderAction, renderAction,
activeTool,
}: { }: {
appState: AppState; appState: AppState;
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
renderAction: ActionManager["renderAction"]; renderAction: ActionManager["renderAction"];
activeTool: AppState["activeTool"]["type"];
}) => { }) => {
const targetElements = getTargetElements( const targetElements = getTargetElements(
getNonDeletedElements(elements), getNonDeletedElements(elements),
@@ -56,13 +56,13 @@ export const SelectedShapeActions = ({
const isRTL = document.documentElement.getAttribute("dir") === "rtl"; const isRTL = document.documentElement.getAttribute("dir") === "rtl";
const showFillIcons = const showFillIcons =
hasBackground(appState.activeTool.type) || hasBackground(activeTool) ||
targetElements.some( targetElements.some(
(element) => (element) =>
hasBackground(element.type) && !isTransparent(element.backgroundColor), hasBackground(element.type) && !isTransparent(element.backgroundColor),
); );
const showChangeBackgroundIcons = const showChangeBackgroundIcons =
hasBackground(appState.activeTool.type) || hasBackground(activeTool) ||
targetElements.some((element) => hasBackground(element.type)); targetElements.some((element) => hasBackground(element.type));
const showLinkIcon = const showLinkIcon =
@@ -79,23 +79,23 @@ export const SelectedShapeActions = ({
return ( return (
<div className="panelColumn"> <div className="panelColumn">
{((hasStrokeColor(appState.activeTool.type) && {((hasStrokeColor(activeTool) &&
appState.activeTool.type !== "image" && activeTool !== "image" &&
commonSelectedType !== "image") || commonSelectedType !== "image") ||
targetElements.some((element) => hasStrokeColor(element.type))) && targetElements.some((element) => hasStrokeColor(element.type))) &&
renderAction("changeStrokeColor")} renderAction("changeStrokeColor")}
{showChangeBackgroundIcons && renderAction("changeBackgroundColor")} {showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
{showFillIcons && renderAction("changeFillStyle")} {showFillIcons && renderAction("changeFillStyle")}
{(hasStrokeWidth(appState.activeTool.type) || {(hasStrokeWidth(activeTool) ||
targetElements.some((element) => hasStrokeWidth(element.type))) && targetElements.some((element) => hasStrokeWidth(element.type))) &&
renderAction("changeStrokeWidth")} renderAction("changeStrokeWidth")}
{(appState.activeTool.type === "freedraw" || {(activeTool === "freedraw" ||
targetElements.some((element) => element.type === "freedraw")) && targetElements.some((element) => element.type === "freedraw")) &&
renderAction("changeStrokeShape")} renderAction("changeStrokeShape")}
{(hasStrokeStyle(appState.activeTool.type) || {(hasStrokeStyle(activeTool) ||
targetElements.some((element) => hasStrokeStyle(element.type))) && ( targetElements.some((element) => hasStrokeStyle(element.type))) && (
<> <>
{renderAction("changeStrokeStyle")} {renderAction("changeStrokeStyle")}
@@ -103,12 +103,12 @@ export const SelectedShapeActions = ({
</> </>
)} )}
{(canChangeSharpness(appState.activeTool.type) || {(canChangeSharpness(activeTool) ||
targetElements.some((element) => canChangeSharpness(element.type))) && ( targetElements.some((element) => canChangeSharpness(element.type))) && (
<>{renderAction("changeSharpness")}</> <>{renderAction("changeSharpness")}</>
)} )}
{(hasText(appState.activeTool.type) || {(hasText(activeTool) ||
targetElements.some((element) => hasText(element.type))) && ( targetElements.some((element) => hasText(element.type))) && (
<> <>
{renderAction("changeFontSize")} {renderAction("changeFontSize")}
@@ -123,7 +123,7 @@ export const SelectedShapeActions = ({
(element) => (element) =>
hasBoundTextElement(element) || isBoundToContainer(element), hasBoundTextElement(element) || isBoundToContainer(element),
) && renderAction("changeVerticalAlign")} ) && renderAction("changeVerticalAlign")}
{(canHaveArrowheads(appState.activeTool.type) || {(canHaveArrowheads(activeTool) ||
targetElements.some((element) => canHaveArrowheads(element.type))) && ( targetElements.some((element) => canHaveArrowheads(element.type))) && (
<>{renderAction("changeArrowhead")}</> <>{renderAction("changeArrowhead")}</>
)} )}
@@ -271,45 +271,3 @@ export const ZoomActions = ({
</Stack.Row> </Stack.Row>
</Stack.Col> </Stack.Col>
); );
export const UndoRedoActions = ({
renderAction,
className,
}: {
renderAction: ActionManager["renderAction"];
className?: string;
}) => (
<div className={`undo-redo-buttons ${className}`}>
{renderAction("undo", { size: "small" })}
{renderAction("redo", { size: "small" })}
</div>
);
export const ExitZenModeAction = ({
executeAction,
showExitZenModeBtn,
}: {
executeAction: ActionManager["executeAction"];
showExitZenModeBtn: boolean;
}) => (
<button
className={clsx("disable-zen-mode", {
"disable-zen-mode--visible": showExitZenModeBtn,
})}
onClick={() => executeAction(actionToggleZenMode)}
>
{t("buttons.exitZenMode")}
</button>
);
export const FinalizeAction = ({
renderAction,
className,
}: {
renderAction: ActionManager["renderAction"];
className?: string;
}) => (
<div className={`finalize-button ${className}`}>
{renderAction("finalize", { size: "small" })}
</div>
);
+178 -89
View File
@@ -264,6 +264,101 @@ import {
} from "../element/Hyperlink"; } from "../element/Hyperlink";
import { shouldShowBoundingBox } from "../element/transformHandles"; import { shouldShowBoundingBox } from "../element/transformHandles";
let TIMES_AGGR: Record<string, { t: number; times: number[] }> = {};
let TIMES_AVG: Record<
string,
{ t: number; times: number[]; avg: number | null }
> = {};
window.DEBUG_LOG_TIMES = true;
let lastDebugLogCall = 0;
let DEBUG_LOG_INTERVAL_ID: null | number = null;
const setupInterval = () => {
if (DEBUG_LOG_INTERVAL_ID === null) {
console.info("%c(starting perf recording)", "color: lime");
DEBUG_LOG_INTERVAL_ID = window.setInterval(debugLogger, 1000);
}
lastDebugLogCall = Date.now();
};
const lessPrecise = (num: number, precision = 5) =>
parseFloat(num.toPrecision(precision));
const getAvgFrameTime = (times: number[]) =>
lessPrecise(times.reduce((a, b) => a + b) / times.length);
const getFps = (frametime: number) => lessPrecise(1000 / frametime);
const debugLogger = () => {
if (Date.now() - lastDebugLogCall > 600 && DEBUG_LOG_INTERVAL_ID !== null) {
window.clearInterval(DEBUG_LOG_INTERVAL_ID);
DEBUG_LOG_INTERVAL_ID = null;
for (const [name, { avg }] of Object.entries(TIMES_AVG)) {
if (avg != null) {
console.info(
`%c${name} run avg: ${avg}ms (${getFps(avg)} fps)`,
"color: blue",
);
}
}
console.info("%c(stopping perf recording)", "color: red");
TIMES_AGGR = {};
TIMES_AVG = {};
return;
}
if (window.DEBUG_LOG_TIMES) {
for (const [name, { t, times }] of Object.entries(TIMES_AGGR)) {
if (times.length) {
console.info(
name,
lessPrecise(times.reduce((a, b) => a + b) / times.length),
times.sort((a, b) => a - b).map((x) => lessPrecise(x)),
);
TIMES_AGGR[name] = { t, times: [] };
}
}
for (const [name, { t, times, avg }] of Object.entries(TIMES_AVG)) {
if (times.length) {
const avgFrameTime = getAvgFrameTime(times);
console.info(name, `${avgFrameTime}ms (${getFps(avgFrameTime)} fps)`);
TIMES_AVG[name] = {
t,
times: [],
avg:
avg != null ? getAvgFrameTime([avg, avgFrameTime]) : avgFrameTime,
};
}
}
}
};
window.logTime = (name: string, time?: number) => {
setupInterval();
const now = performance.now();
const { t, times } = (TIMES_AGGR[name] = TIMES_AGGR[name] || {
t: 0,
times: [],
});
if (t) {
times.push(time != null ? time : now - t);
}
TIMES_AGGR[name].t = now;
};
window.logTimeAverage = (name: string, time?: number) => {
setupInterval();
const now = performance.now();
const { t, times } = (TIMES_AVG[name] = TIMES_AVG[name] || {
t: 0,
times: [],
});
if (t) {
times.push(time != null ? time : now - t);
}
TIMES_AVG[name].t = now;
};
const deviceContextInitialValue = { const deviceContextInitialValue = {
isSmScreen: false, isSmScreen: false,
isMobile: false, isMobile: false,
@@ -272,7 +367,6 @@ const deviceContextInitialValue = {
}; };
const DeviceContext = React.createContext<Device>(deviceContextInitialValue); const DeviceContext = React.createContext<Device>(deviceContextInitialValue);
export const useDevice = () => useContext<Device>(DeviceContext); 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;
@@ -280,22 +374,6 @@ const ExcalidrawContainerContext = React.createContext<{
export const useExcalidrawContainer = () => export const useExcalidrawContainer = () =>
useContext(ExcalidrawContainerContext); useContext(ExcalidrawContainerContext);
const ExcalidrawElementsContext = React.createContext<
readonly NonDeletedExcalidrawElement[]
>([]);
const ExcalidrawAppStateContext = React.createContext<AppState>({
...getDefaultAppState(),
width: 0,
height: 0,
offsetLeft: 0,
offsetTop: 0,
});
export const useExcalidrawElements = () =>
useContext(ExcalidrawElementsContext);
export const useExcalidrawAppState = () =>
useContext(ExcalidrawAppStateContext);
let didTapTwice: boolean = false; let didTapTwice: boolean = false;
let tappedTwiceTimer = 0; let tappedTwiceTimer = 0;
let cursorX = 0; let cursorX = 0;
@@ -493,6 +571,26 @@ class App extends React.Component<AppProps, AppState> {
); );
} }
private __renderUI = true;
private perfTest = (runs = 180, initial = true) => {
if (initial) {
console.time("perfTest");
} else if (!runs) {
console.timeEnd("perfTest");
}
if (runs) {
requestAnimationFrame((id) => {
for (const element of this.scene.getNonDeletedElements()) {
mutateElement(element, {
x: element.x + 1,
});
}
this.perfTest(runs - 1, false);
});
}
};
public render() { public render() {
const selectedElement = getSelectedElements( const selectedElement = getSelectedElements(
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
@@ -522,69 +620,65 @@ class App extends React.Component<AppProps, AppState> {
value={this.excalidrawContainerValue} value={this.excalidrawContainerValue}
> >
<DeviceContext.Provider value={this.device}> <DeviceContext.Provider value={this.device}>
<ExcalidrawAppStateContext.Provider value={this.state}> {this.__renderUI && (
<ExcalidrawElementsContext.Provider <LayerUI
value={this.scene.getNonDeletedElements()} canvas={this.canvas}
> appState={this.state}
<LayerUI files={this.files}
canvas={this.canvas} setAppState={this.setAppState}
appState={this.state} actionManager={this.actionManager}
files={this.files} elements={this.scene.getNonDeletedElements()}
setAppState={this.setAppState} onCollabButtonClick={onCollabButtonClick}
actionManager={this.actionManager} onLockToggle={this.toggleLock}
elements={this.scene.getNonDeletedElements()} onPenModeToggle={this.togglePenMode}
onCollabButtonClick={onCollabButtonClick} onInsertElements={(elements) =>
onLockToggle={this.toggleLock} this.addElementsFromPasteOrLibrary({
onPenModeToggle={this.togglePenMode} elements,
onInsertElements={(elements) => position: "center",
this.addElementsFromPasteOrLibrary({ files: null,
elements, })
position: "center", }
files: null, langCode={getLanguage().code}
}) isCollaborating={this.props.isCollaborating}
} renderTopRightUI={renderTopRightUI}
langCode={getLanguage().code} renderCustomFooter={renderFooter}
isCollaborating={this.props.isCollaborating} renderCustomStats={renderCustomStats}
renderTopRightUI={renderTopRightUI} showExitZenModeBtn={
renderCustomFooter={renderFooter} typeof this.props?.zenModeEnabled === "undefined" &&
renderCustomStats={renderCustomStats} this.state.zenModeEnabled
showExitZenModeBtn={ }
typeof this.props?.zenModeEnabled === "undefined" && showThemeBtn={
this.state.zenModeEnabled typeof this.props?.theme === "undefined" &&
} this.props.UIOptions.canvasActions.theme
showThemeBtn={ }
typeof this.props?.theme === "undefined" && libraryReturnUrl={this.props.libraryReturnUrl}
this.props.UIOptions.canvasActions.theme UIOptions={this.props.UIOptions}
} focusContainer={this.focusContainer}
libraryReturnUrl={this.props.libraryReturnUrl} library={this.library}
UIOptions={this.props.UIOptions} id={this.id}
focusContainer={this.focusContainer} onImageAction={this.onImageAction}
library={this.library} />
id={this.id} )}
onImageAction={this.onImageAction} <div className="excalidraw-textEditorContainer" />
/> <div className="excalidraw-contextMenuContainer" />
<div className="excalidraw-textEditorContainer" /> {selectedElement.length === 1 && this.state.showHyperlinkPopup && (
<div className="excalidraw-contextMenuContainer" /> <Hyperlink
{selectedElement.length === 1 && key={selectedElement[0].id}
this.state.showHyperlinkPopup && ( element={selectedElement[0]}
<Hyperlink appState={this.state}
key={selectedElement[0].id} setAppState={this.setAppState}
element={selectedElement[0]} onLinkOpen={this.props.onLinkOpen}
setAppState={this.setAppState} />
onLinkOpen={this.props.onLinkOpen} )}
/> {this.state.toast !== null && (
)} <Toast
{this.state.toast !== null && ( message={this.state.toast.message}
<Toast onClose={() => this.setToast(null)}
message={this.state.toast.message} duration={this.state.toast.duration}
onClose={() => this.setToast(null)} closable={this.state.toast.closable}
duration={this.state.toast.duration} />
closable={this.state.toast.closable} )}
/> <main>{this.renderCanvas()}</main>
)}
<main>{this.renderCanvas()}</main>
</ExcalidrawElementsContext.Provider>{" "}
</ExcalidrawAppStateContext.Provider>
</DeviceContext.Provider> </DeviceContext.Provider>
</ExcalidrawContainerContext.Provider> </ExcalidrawContainerContext.Provider>
</div> </div>
@@ -843,7 +937,8 @@ class App extends React.Component<AppProps, AppState> {
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 ||
process.env.REACT_APP_VERCEL_ENV === "preview"
) { ) {
const setState = this.setState.bind(this); const setState = this.setState.bind(this);
Object.defineProperties(window.h, { Object.defineProperties(window.h, {
@@ -6166,13 +6261,6 @@ class App extends React.Component<AppProps, AppState> {
return; return;
} }
if (width === 0 || height === 0) {
if (cb) {
cb();
}
return;
}
this.setState( this.setState(
{ {
width, width,
@@ -6233,7 +6321,8 @@ declare global {
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 ||
process.env.REACT_APP_VERCEL_ENV === "preview"
) { ) {
window.h = window.h || ({} as Window["h"]); window.h = window.h || ({} as Window["h"]);
-2
View File
@@ -343,8 +343,6 @@ const ColorInput = React.forwardRef(
}, },
); );
ColorInput.displayName = "ColorInput";
export const ColorPicker = ({ export const ColorPicker = ({
type, type,
color, color,
-99
View File
@@ -1,99 +0,0 @@
import clsx from "clsx";
import { ActionManager } from "../actions/manager";
import { AppState, ExcalidrawProps } from "../types";
import {
ExitZenModeAction,
FinalizeAction,
UndoRedoActions,
ZoomActions,
} from "./Actions";
import { useDevice } from "./App";
import { Island } from "./Island";
import { Section } from "./Section";
import Stack from "./Stack";
const Footer = ({
appState,
actionManager,
renderCustomFooter,
showExitZenModeBtn,
}: {
appState: AppState;
actionManager: ActionManager;
renderCustomFooter?: ExcalidrawProps["renderFooter"];
showExitZenModeBtn: boolean;
}) => {
const device = useDevice();
const showFinalize =
!appState.viewModeEnabled && appState.multiElement && device.isTouchScreen;
return (
<footer
role="contentinfo"
className="layer-ui__wrapper__footer App-menu App-menu_bottom"
>
<div
className={clsx("layer-ui__wrapper__footer-left zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-left":
appState.zenModeEnabled,
})}
>
<Stack.Col gap={2}>
<Section heading="canvasActions">
<Island padding={1}>
<ZoomActions
renderAction={actionManager.renderAction}
zoom={appState.zoom}
/>
</Island>
{!appState.viewModeEnabled && (
<>
<UndoRedoActions
renderAction={actionManager.renderAction}
className={clsx("zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-bottom":
appState.zenModeEnabled,
})}
/>
<div
className={clsx("eraser-buttons zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-left":
appState.zenModeEnabled,
})}
>
{actionManager.renderAction("eraser", { size: "small" })}
</div>
</>
)}
{showFinalize && (
<FinalizeAction
renderAction={actionManager.renderAction}
className={clsx("zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-left":
appState.zenModeEnabled,
})}
/>
)}
</Section>
</Stack.Col>
</div>
<div
className={clsx(
"layer-ui__wrapper__footer-center zen-mode-transition",
{
"layer-ui__wrapper__footer-left--transition-bottom":
appState.zenModeEnabled,
},
)}
>
{renderCustomFooter?.(false, appState)}
</div>
<ExitZenModeAction
executeAction={actionManager.executeAction}
showExitZenModeBtn={showExitZenModeBtn}
/>
</footer>
);
};
export default Footer;
+177 -75
View File
@@ -10,7 +10,7 @@ import { calculateScrollCenter, getSelectedElements } from "../scene";
import { ExportType } from "../scene/types"; import { ExportType } from "../scene/types";
import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types"; import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types";
import { muteFSAbortError } from "../utils"; import { muteFSAbortError } from "../utils";
import { SelectedShapeActions, ShapesSwitcher } from "./Actions"; import { SelectedShapeActions, ShapesSwitcher, ZoomActions } from "./Actions";
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle"; import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
import CollabButton from "./CollabButton"; import CollabButton from "./CollabButton";
import { ErrorDialog } from "./ErrorDialog"; import { ErrorDialog } from "./ErrorDialog";
@@ -39,7 +39,7 @@ import { trackEvent } from "../analytics";
import { useDevice } from "../components/App"; import { useDevice } from "../components/App";
import { Stats } from "./Stats"; import { Stats } from "./Stats";
import { actionToggleStats } from "../actions/actionToggleStats"; import { actionToggleStats } from "../actions/actionToggleStats";
import Footer from "./Footer"; import { actionToggleZenMode } from "../actions";
interface LayerUIProps { interface LayerUIProps {
actionManager: ActionManager; actionManager: ActionManager;
@@ -71,8 +71,8 @@ const LayerUI = ({
appState, appState,
files, files,
setAppState, setAppState,
elements,
canvas, canvas,
elements,
onCollabButtonClick, onCollabButtonClick,
onLockToggle, onLockToggle,
onPenModeToggle, onPenModeToggle,
@@ -210,8 +210,8 @@ const LayerUI = ({
)} )}
</Stack.Row> </Stack.Row>
<BackgroundPickerAndDarkModeToggle <BackgroundPickerAndDarkModeToggle
appState={appState}
actionManager={actionManager} actionManager={actionManager}
appState={appState}
setAppState={setAppState} setAppState={setAppState}
showThemeBtn={showThemeBtn} showThemeBtn={showThemeBtn}
/> />
@@ -244,6 +244,7 @@ const LayerUI = ({
appState={appState} appState={appState}
elements={elements} elements={elements}
renderAction={actionManager.renderAction} renderAction={actionManager.renderAction}
activeTool={appState.activeTool.type}
/> />
</Island> </Island>
</Section> </Section>
@@ -278,6 +279,7 @@ const LayerUI = ({
libraryReturnUrl={libraryReturnUrl} libraryReturnUrl={libraryReturnUrl}
focusContainer={focusContainer} focusContainer={focusContainer}
library={library} library={library}
theme={appState.theme}
files={files} files={files}
id={id} id={id}
appState={appState} appState={appState}
@@ -381,7 +383,100 @@ const LayerUI = ({
); );
}; };
return ( const renderBottomAppMenu = () => {
return (
<footer
role="contentinfo"
className="layer-ui__wrapper__footer App-menu App-menu_bottom"
>
<div
className={clsx(
"layer-ui__wrapper__footer-left zen-mode-transition",
{
"layer-ui__wrapper__footer-left--transition-left":
appState.zenModeEnabled,
},
)}
>
<Stack.Col gap={2}>
<Section heading="canvasActions">
<Island padding={1}>
<ZoomActions
renderAction={actionManager.renderAction}
zoom={appState.zoom}
/>
</Island>
{!appState.viewModeEnabled && (
<>
<div
className={clsx("undo-redo-buttons zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-bottom":
appState.zenModeEnabled,
})}
>
{actionManager.renderAction("undo", { size: "small" })}
{actionManager.renderAction("redo", { size: "small" })}
</div>
<div
className={clsx("eraser-buttons zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-left":
appState.zenModeEnabled,
})}
>
{actionManager.renderAction("eraser", { size: "small" })}
</div>
</>
)}
{!appState.viewModeEnabled &&
appState.multiElement &&
device.isTouchScreen && (
<div
className={clsx("finalize-button zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-left":
appState.zenModeEnabled,
})}
>
{actionManager.renderAction("finalize", { size: "small" })}
</div>
)}
</Section>
</Stack.Col>
</div>
<div
className={clsx(
"layer-ui__wrapper__footer-center zen-mode-transition",
{
"layer-ui__wrapper__footer-left--transition-bottom":
appState.zenModeEnabled,
},
)}
>
{renderCustomFooter?.(false, appState)}
</div>
<div
className={clsx(
"layer-ui__wrapper__footer-right zen-mode-transition",
{
"transition-right disable-pointerEvents": appState.zenModeEnabled,
},
)}
>
{actionManager.renderAction("toggleShortcuts")}
</div>
<button
className={clsx("disable-zen-mode", {
"disable-zen-mode--visible": showExitZenModeBtn,
})}
onClick={() => actionManager.executeAction(actionToggleZenMode)}
>
{t("buttons.exitZenMode")}
</button>
</footer>
);
};
const dialogs = (
<> <>
{appState.isLoading && <LoadingMessage delay={250} />} {appState.isLoading && <LoadingMessage delay={250} />}
{appState.errorMessage && ( {appState.errorMessage && (
@@ -409,77 +504,84 @@ const LayerUI = ({
} }
/> />
)} )}
{device.isMobile && ( </>
<MobileMenu );
appState={appState}
elements={elements}
actionManager={actionManager}
libraryMenu={libraryMenu}
renderJSONExportDialog={renderJSONExportDialog}
renderImageExportDialog={renderImageExportDialog}
setAppState={setAppState}
onCollabButtonClick={onCollabButtonClick}
onLockToggle={() => onLockToggle()}
onPenModeToggle={onPenModeToggle}
canvas={canvas}
isCollaborating={isCollaborating}
renderCustomFooter={renderCustomFooter}
showThemeBtn={showThemeBtn}
onImageAction={onImageAction}
renderTopRightUI={renderTopRightUI}
renderCustomStats={renderCustomStats}
/>
)}
{!device.isMobile && ( const renderStats = () => {
<div if (!appState.showStats) {
className={clsx("layer-ui__wrapper", { return null;
"disable-pointerEvents": }
appState.draggingElement || return (
appState.resizingElement || <Stats
(appState.editingElement && appState={appState}
!isTextElement(appState.editingElement)), setAppState={setAppState}
})} elements={elements}
style={ onClose={() => {
appState.isLibraryOpen && actionManager.executeAction(actionToggleStats);
appState.isLibraryMenuDocked && }}
device.canDeviceFitSidebar renderCustomStats={renderCustomStats}
? { width: `calc(100% - ${LIBRARY_SIDEBAR_WIDTH}px)` } />
: {} );
} };
>
{renderFixedSideContainer()} return device.isMobile ? (
<Footer <>
appState={appState} {dialogs}
actionManager={actionManager} <MobileMenu
renderCustomFooter={renderCustomFooter} appState={appState}
showExitZenModeBtn={showExitZenModeBtn} elements={elements}
/> actionManager={actionManager}
{appState.showStats && ( libraryMenu={libraryMenu}
<Stats renderJSONExportDialog={renderJSONExportDialog}
appState={appState} renderImageExportDialog={renderImageExportDialog}
setAppState={setAppState} setAppState={setAppState}
elements={elements} onCollabButtonClick={onCollabButtonClick}
onClose={() => { onLockToggle={() => onLockToggle()}
actionManager.executeAction(actionToggleStats); onPenModeToggle={onPenModeToggle}
}} canvas={canvas}
renderCustomStats={renderCustomStats} isCollaborating={isCollaborating}
/> renderCustomFooter={renderCustomFooter}
)} showThemeBtn={showThemeBtn}
{appState.scrolledOutside && ( onImageAction={onImageAction}
<button renderTopRightUI={renderTopRightUI}
className="scroll-back-to-content" renderStats={renderStats}
onClick={() => { />
setAppState({ </>
...calculateScrollCenter(elements, appState, canvas), ) : (
}); <>
}} <div
> className={clsx("layer-ui__wrapper", {
{t("buttons.scrollBackToContent")} "disable-pointerEvents":
</button> appState.draggingElement ||
)} appState.resizingElement ||
</div> (appState.editingElement &&
)} !isTextElement(appState.editingElement)),
})}
style={
appState.isLibraryOpen &&
appState.isLibraryMenuDocked &&
device.canDeviceFitSidebar
? { width: `calc(100% - ${LIBRARY_SIDEBAR_WIDTH}px)` }
: {}
}
>
{dialogs}
{renderFixedSideContainer()}
{renderBottomAppMenu()}
{renderStats()}
{appState.scrolledOutside && (
<button
className="scroll-back-to-content"
onClick={() => {
setAppState({
...calculateScrollCenter(elements, appState, canvas),
});
}}
>
{t("buttons.scrollBackToContent")}
</button>
)}
</div>
{appState.isLibraryOpen && ( {appState.isLibraryOpen && (
<div className="layer-ui__sidebar">{libraryMenu}</div> <div className="layer-ui__sidebar">{libraryMenu}</div>
)} )}
+4 -1
View File
@@ -80,6 +80,7 @@ export const LibraryMenu = ({
onInsertLibraryItems, onInsertLibraryItems,
pendingElements, pendingElements,
onAddToLibrary, onAddToLibrary,
theme,
setAppState, setAppState,
files, files,
libraryReturnUrl, libraryReturnUrl,
@@ -92,6 +93,7 @@ export const LibraryMenu = ({
onClose: () => void; onClose: () => void;
onInsertLibraryItems: (libraryItems: LibraryItems) => void; onInsertLibraryItems: (libraryItems: LibraryItems) => void;
onAddToLibrary: () => void; onAddToLibrary: () => void;
theme: AppState["theme"];
files: BinaryFiles; files: BinaryFiles;
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
@@ -103,6 +105,7 @@ export const LibraryMenu = ({
const ref = useRef<HTMLDivElement | null>(null); const ref = useRef<HTMLDivElement | null>(null);
const device = useDevice(); const device = useDevice();
useOnClickOutside( useOnClickOutside(
ref, ref,
useCallback( useCallback(
@@ -287,7 +290,7 @@ export const LibraryMenu = ({
appState={appState} appState={appState}
libraryReturnUrl={libraryReturnUrl} libraryReturnUrl={libraryReturnUrl}
library={library} library={library}
theme={appState.theme} theme={theme}
files={files} files={files}
id={id} id={id}
selectedItems={selectedItems} selectedItems={selectedItems}
+5 -16
View File
@@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { AppState, ExcalidrawProps } from "../types"; import { AppState } from "../types";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { t } from "../i18n"; import { t } from "../i18n";
import Stack from "./Stack"; import Stack from "./Stack";
@@ -18,8 +18,6 @@ import { UserList } from "./UserList";
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle"; import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
import { LibraryButton } from "./LibraryButton"; import { LibraryButton } from "./LibraryButton";
import { PenModeButton } from "./PenModeButton"; import { PenModeButton } from "./PenModeButton";
import { Stats } from "./Stats";
import { actionToggleStats } from "../actions";
type MobileMenuProps = { type MobileMenuProps = {
appState: AppState; appState: AppState;
@@ -44,7 +42,7 @@ type MobileMenuProps = {
isMobile: boolean, isMobile: boolean,
appState: AppState, appState: AppState,
) => JSX.Element | null; ) => JSX.Element | null;
renderCustomStats?: ExcalidrawProps["renderCustomStats"]; renderStats: () => JSX.Element | null;
}; };
export const MobileMenu = ({ export const MobileMenu = ({
@@ -64,7 +62,7 @@ export const MobileMenu = ({
showThemeBtn, showThemeBtn,
onImageAction, onImageAction,
renderTopRightUI, renderTopRightUI,
renderCustomStats, renderStats,
}: MobileMenuProps) => { }: MobileMenuProps) => {
const renderToolbar = () => { const renderToolbar = () => {
return ( return (
@@ -186,17 +184,7 @@ export const MobileMenu = ({
return ( return (
<> <>
{!appState.viewModeEnabled && renderToolbar()} {!appState.viewModeEnabled && renderToolbar()}
{!appState.openMenu && appState.showStats && ( {renderStats()}
<Stats
appState={appState}
setAppState={setAppState}
elements={elements}
onClose={() => {
actionManager.executeAction(actionToggleStats);
}}
renderCustomStats={renderCustomStats}
/>
)}
<div <div
className="App-bottom-bar" className="App-bottom-bar"
style={{ style={{
@@ -233,6 +221,7 @@ export const MobileMenu = ({
appState={appState} appState={appState}
elements={elements} elements={elements}
renderAction={actionManager.renderAction} renderAction={actionManager.renderAction}
activeTool={appState.activeTool.type}
/> />
</Section> </Section>
) : null} ) : null}
+5 -1
View File
@@ -2,6 +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 { 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";
@@ -15,10 +16,13 @@ export const Stats = (props: {
onClose: () => void; onClose: () => void;
renderCustomStats: ExcalidrawProps["renderCustomStats"]; renderCustomStats: ExcalidrawProps["renderCustomStats"];
}) => { }) => {
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) {
return null;
}
return ( return (
<div className="Stats"> <div className="Stats">
<Island padding={2}> <Island padding={2}>
-2
View File
@@ -187,5 +187,3 @@ ToolButton.defaultProps = {
className: "", className: "",
size: "medium", size: "medium",
}; };
ToolButton.displayName = "ToolButton";
+2 -3
View File
@@ -32,7 +32,6 @@ import { getElementAbsoluteCoords } from "./";
import "./Hyperlink.scss"; import "./Hyperlink.scss";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { useExcalidrawAppState } from "../components/App";
const CONTAINER_WIDTH = 320; const CONTAINER_WIDTH = 320;
const SPACE_BOTTOM = 85; const SPACE_BOTTOM = 85;
@@ -49,15 +48,15 @@ let IS_HYPERLINK_TOOLTIP_VISIBLE = false;
export const Hyperlink = ({ export const Hyperlink = ({
element, element,
appState,
setAppState, setAppState,
onLinkOpen, onLinkOpen,
}: { }: {
element: NonDeletedExcalidrawElement; element: NonDeletedExcalidrawElement;
appState: AppState;
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
onLinkOpen: ExcalidrawProps["onLinkOpen"]; onLinkOpen: ExcalidrawProps["onLinkOpen"];
}) => { }) => {
const appState = useExcalidrawAppState();
const linkVal = element.link || ""; const linkVal = element.link || "";
const [inputVal, setInputVal] = useState(linkVal); const [inputVal, setInputVal] = useState(linkVal);
+3 -10
View File
@@ -107,19 +107,12 @@ const solveQuadratic = (
return false; return false;
} }
const t1 = (-b + Math.sqrt(sqrtPart)) / (2 * a);
const t2 = (-b - Math.sqrt(sqrtPart)) / (2 * a);
let s1 = null; let s1 = null;
let s2 = null; let s2 = null;
let t1 = Infinity;
let t2 = Infinity;
if (a === 0) {
t1 = t2 = -c / b;
} else {
t1 = (-b + Math.sqrt(sqrtPart)) / (2 * a);
t2 = (-b - Math.sqrt(sqrtPart)) / (2 * a);
}
if (t1 >= 0 && t1 <= 1) { if (t1 >= 0 && t1 <= 1) {
s1 = getBezierValueForT(t1, p0, p1, p2, p3); s1 = getBezierValueForT(t1, p0, p1, p2, p3);
} }
+1 -3
View File
@@ -686,9 +686,7 @@ export class LinearElementEditor {
const point = element.points[index]; const point = element.points[index];
const { x, y } = element; const { x, y } = element;
return point return rotate(x + point[0], y + point[1], cx, cy, element.angle);
? rotate(x + point[0], y + point[1], cx, cy, element.angle)
: rotate(x, y, cx, cy, element.angle);
} }
static pointFromAbsoluteCoords( static pointFromAbsoluteCoords(
+1 -5
View File
@@ -7,8 +7,6 @@ import {
import { DEFAULT_VERSION } from "../constants"; import { DEFAULT_VERSION } from "../constants";
import { t } from "../i18n"; import { t } from "../i18n";
import { copyTextToSystemClipboard } from "../clipboard"; import { copyTextToSystemClipboard } from "../clipboard";
import { AppState } from "../types";
import { NonDeletedExcalidrawElement } from "../element/types";
type StorageSizes = { scene: number; total: number }; type StorageSizes = { scene: number; total: number };
const STORAGE_SIZE_TIMEOUT = 500; const STORAGE_SIZE_TIMEOUT = 500;
@@ -22,8 +20,6 @@ const getStorageSizes = debounce((cb: (sizes: StorageSizes) => void) => {
type Props = { type Props = {
setToast: (message: string) => void; setToast: (message: string) => void;
elements: readonly NonDeletedExcalidrawElement[];
appState: AppState;
}; };
const CustomStats = (props: Props) => { const CustomStats = (props: Props) => {
const [storageSizes, setStorageSizes] = useState<StorageSizes>({ const [storageSizes, setStorageSizes] = useState<StorageSizes>({
@@ -35,7 +31,7 @@ const CustomStats = (props: Props) => {
getStorageSizes((sizes) => { getStorageSizes((sizes) => {
setStorageSizes(sizes); setStorageSizes(sizes);
}); });
}, [props.elements, props.appState]); });
useEffect(() => () => getStorageSizes.cancel(), []); useEffect(() => () => getStorageSizes.cancel(), []);
const version = getVersion(); const version = getVersion();
+2 -1
View File
@@ -169,7 +169,8 @@ class Collab extends PureComponent<Props, CollabState> {
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 ||
process.env.REACT_APP_VERCEL_ENV === "preview"
) { ) {
window.collab = window.collab || ({} as Window["collab"]); window.collab = window.collab || ({} as Window["collab"]);
Object.defineProperties(window, { Object.defineProperties(window, {
+1 -6
View File
@@ -666,15 +666,10 @@ const ExcalidrawWrapper = () => {
[langCode], [langCode],
); );
const renderCustomStats = ( const renderCustomStats = () => {
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
) => {
return ( return (
<CustomStats <CustomStats
setToast={(message) => excalidrawAPI!.setToast({ message })} setToast={(message) => excalidrawAPI!.setToast({ message })}
appState={appState}
elements={elements}
/> />
); );
}; };
+3
View File
@@ -16,6 +16,9 @@ interface Window {
EXCALIDRAW_EXPORT_SOURCE: string; EXCALIDRAW_EXPORT_SOURCE: string;
EXCALIDRAW_THROTTLE_RENDER: boolean | undefined; EXCALIDRAW_THROTTLE_RENDER: boolean | undefined;
gtag: Function; gtag: Function;
logTime: (name: string, time?: number) => void;
logTimeAverage: (name: string, time?: number) => void;
DEBUG_LOG_TIMES: boolean;
} }
// https://github.com/facebook/create-react-app/blob/ddcb7d5/packages/react-scripts/lib/react-app.d.ts // https://github.com/facebook/create-react-app/blob/ddcb7d5/packages/react-scripts/lib/react-app.d.ts
-2
View File
@@ -29,8 +29,6 @@ Please add the latest change on the top under the correct section.
#### Features #### Features
- [`loadLibraryFromBlob`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#loadLibraryFromBlob) now takes an additional parameter `defaultStatus` which sets the default status of library item if not present, defaults to `unpublished` [#5067](https://github.com/excalidraw/excalidraw/pull/5067).
- 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). - 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) - 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)
+7 -7
View File
@@ -482,8 +482,8 @@ You can add `customData` to elements when passing them as `initialData`, or usin
You can pass a `ref` when you want to access some excalidraw APIs. We expose the below APIs: You can pass a `ref` when you want to access some excalidraw APIs. We expose the below APIs:
| API | Signature | Usage | | API | signature | Usage |
| --- | --- | --- | | --- | --- | --- | --- |
| 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) | <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 | | [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 |
@@ -496,11 +496,11 @@ You can pass a `ref` when you want to access some excalidraw APIs. We expose the
| 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 | <code> (target?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement</a> &#124; <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. | | scrollToContent | <code> (target?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement</a> &#124; <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) | <code>(url: string, token?: string) => void</code> | Imports library from given URL | | [importLibrary](#importlibrary) | `(url: string, token?: string) => void` | Imports library from given URL |
| [setToast](#setToast) | <code>({ message: string, closable?:boolean, duration?:number } &#124; null) => void</code> | This API can be used to show the toast with custom message. | | [setToast](#setToast) | `({message: string, closable?:boolean, duration?:number} | null) => 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) | <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. | | [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"]&#124; "eraser" } &#124; { type: "custom"; customType: string }) => void</code> | This API can be used to set the active tool | | [setActiveTool](#setActiveTool) | <code>(tool: { type: typeof <a href="https://github.com/excalidraw/excalidraw/blob/master/src/shapes.tsx#L4">SHAPES</a>[number]["value"] &#124; "eraser" } &#124; { 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 | | [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 | | [resetCursor](#resetCursor) | <code>() => void </code> | This API can be used to reset to default mouse cursor on the canvas |
@@ -1101,10 +1101,10 @@ import { loadLibraryFromBlob } from "@excalidraw/excalidraw";
**_Signature_** **_Signature_**
<pre> <pre>
loadLibraryFromBlob(blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>, defaultStatus: "published" | "unpublished") loadLibraryFromBlob(blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>)
</pre> </pre>
This function loads the library from the blob. Additonally takes `defaultStatus` param which sets the default status for library item if not present, defaults to `unpublished`. This function loads the library from the blob.
#### `loadFromBlob` #### `loadFromBlob`
+1
View File
@@ -330,6 +330,7 @@ export const _renderScene = ({
if (canvas === null) { if (canvas === null) {
return { atLeastOneVisibleElement: false }; return { atLeastOneVisibleElement: false };
} }
window.logTimeAverage("renderScene");
const { const {
renderScrollbars = true, renderScrollbars = true,
renderSelection = true, renderSelection = true,
+5 -5
View File
@@ -42,7 +42,7 @@ describe("Test dragCreate", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(7); expect(renderScene).toHaveBeenCalledTimes(8);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
@@ -73,7 +73,7 @@ describe("Test dragCreate", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(7); expect(renderScene).toHaveBeenCalledTimes(8);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
@@ -104,7 +104,7 @@ describe("Test dragCreate", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(7); expect(renderScene).toHaveBeenCalledTimes(8);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
@@ -135,7 +135,7 @@ describe("Test dragCreate", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(7); expect(renderScene).toHaveBeenCalledTimes(8);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
@@ -170,7 +170,7 @@ describe("Test dragCreate", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(7); expect(renderScene).toHaveBeenCalledTimes(8);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
+2 -2
View File
@@ -38,7 +38,7 @@ describe("move element", () => {
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(7); expect(renderScene).toHaveBeenCalledTimes(8);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
@@ -120,7 +120,7 @@ describe("duplicate element on move when ALT is clicked", () => {
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(7); expect(renderScene).toHaveBeenCalledTimes(8);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
+2 -2
View File
@@ -102,7 +102,7 @@ describe("multi point mode in linear elements", () => {
key: KEYS.ENTER, key: KEYS.ENTER,
}); });
expect(renderScene).toHaveBeenCalledTimes(13); expect(renderScene).toHaveBeenCalledTimes(14);
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
const element = h.elements[0] as ExcalidrawLinearElement; const element = h.elements[0] as ExcalidrawLinearElement;
@@ -145,7 +145,7 @@ describe("multi point mode in linear elements", () => {
key: KEYS.ENTER, key: KEYS.ENTER,
}); });
expect(renderScene).toHaveBeenCalledTimes(13); expect(renderScene).toHaveBeenCalledTimes(14);
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
const element = h.elements[0] as ExcalidrawLinearElement; const element = h.elements[0] as ExcalidrawLinearElement;
+119 -7
View File
@@ -2998,7 +2998,7 @@ async-limiter@~1.0.0:
resolved "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz" resolved "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz"
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
async@^2.6.2: async@^2.6.2, async@^2.6.4:
version "2.6.4" version "2.6.4"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221"
integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==
@@ -3269,6 +3269,13 @@ base@^0.11.1:
mixin-deep "^1.2.0" mixin-deep "^1.2.0"
pascalcase "^0.1.1" pascalcase "^0.1.1"
basic-auth@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a"
integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==
dependencies:
safe-buffer "5.1.2"
batch@0.6.1: batch@0.6.1:
version "0.6.1" version "0.6.1"
resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz" resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz"
@@ -3727,6 +3734,14 @@ chalk@^4.0.0, chalk@^4.1.0:
ansi-styles "^4.1.0" ansi-styles "^4.1.0"
supports-color "^7.1.0" supports-color "^7.1.0"
chalk@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
char-regex@^1.0.2: char-regex@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz" resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz"
@@ -4149,6 +4164,11 @@ core-util-is@~1.0.0:
resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
corser@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87"
integrity sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==
cosmiconfig@^5.0.0: cosmiconfig@^5.0.0:
version "5.2.1" version "5.2.1"
resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz" resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz"
@@ -4217,7 +4237,14 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7:
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
sha.js "^2.4.8" sha.js "^2.4.8"
cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: cross-env@7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==
dependencies:
cross-spawn "^7.0.1"
cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "7.0.3" version "7.0.3"
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
@@ -4530,7 +4557,7 @@ debug@4:
dependencies: dependencies:
ms "2.1.2" ms "2.1.2"
debug@^3.1.1, debug@^3.2.6: debug@^3.1.1, debug@^3.2.6, debug@^3.2.7:
version "3.2.7" version "3.2.7"
resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz"
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
@@ -6310,6 +6337,13 @@ html-encoding-sniffer@^2.0.1:
dependencies: dependencies:
whatwg-encoding "^1.0.5" whatwg-encoding "^1.0.5"
html-encoding-sniffer@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9"
integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==
dependencies:
whatwg-encoding "^2.0.0"
html-entities@^1.2.1, html-entities@^1.3.1: html-entities@^1.2.1, html-entities@^1.3.1:
version "1.4.0" version "1.4.0"
resolved "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz" resolved "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz"
@@ -6410,7 +6444,7 @@ http-proxy-middleware@0.19.1:
lodash "^4.17.11" lodash "^4.17.11"
micromatch "^3.1.10" micromatch "^3.1.10"
http-proxy@^1.17.0: http-proxy@^1.17.0, http-proxy@^1.18.1:
version "1.18.1" version "1.18.1"
resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz" resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz"
integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
@@ -6419,6 +6453,25 @@ http-proxy@^1.17.0:
follow-redirects "^1.0.0" follow-redirects "^1.0.0"
requires-port "^1.0.0" requires-port "^1.0.0"
http-server@14.1.1:
version "14.1.1"
resolved "https://registry.yarnpkg.com/http-server/-/http-server-14.1.1.tgz#d60fbb37d7c2fdff0f0fbff0d0ee6670bd285e2e"
integrity sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==
dependencies:
basic-auth "^2.0.1"
chalk "^4.1.2"
corser "^2.0.1"
he "^1.2.0"
html-encoding-sniffer "^3.0.0"
http-proxy "^1.18.1"
mime "^1.6.0"
minimist "^1.2.6"
opener "^1.5.1"
portfinder "^1.0.28"
secure-compare "3.0.1"
union "~0.5.0"
url-join "^4.0.1"
https-browserify@^1.0.0: https-browserify@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz" resolved "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz"
@@ -6461,6 +6514,13 @@ iconv-lite@0.4.24:
dependencies: dependencies:
safer-buffer ">= 2.1.2 < 3" safer-buffer ">= 2.1.2 < 3"
iconv-lite@0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
dependencies:
safer-buffer ">= 2.1.2 < 3.0.0"
icss-utils@^4.0.0, icss-utils@^4.1.1: icss-utils@^4.0.0, icss-utils@^4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz" resolved "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz"
@@ -8175,7 +8235,7 @@ mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.24:
dependencies: dependencies:
mime-db "1.46.0" mime-db "1.46.0"
mime@1.6.0: mime@1.6.0, mime@^1.6.0:
version "1.6.0" version "1.6.0"
resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
@@ -8222,7 +8282,7 @@ minimatch@3.0.4, minimatch@^3.0.4:
dependencies: dependencies:
brace-expansion "^1.1.7" brace-expansion "^1.1.7"
minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
version "1.2.6" version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
@@ -8294,6 +8354,13 @@ mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1:
dependencies: dependencies:
minimist "^1.2.5" minimist "^1.2.5"
mkdirp@^0.5.6:
version "0.5.6"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
dependencies:
minimist "^1.2.6"
mkdirp@^1.0.3, mkdirp@^1.0.4: mkdirp@^1.0.3, mkdirp@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz"
@@ -8704,6 +8771,11 @@ open@^7.0.2:
is-docker "^2.0.0" is-docker "^2.0.0"
is-wsl "^2.1.1" is-wsl "^2.1.1"
opener@^1.5.1:
version "1.5.2"
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==
opn@^5.5.0: opn@^5.5.0:
version "5.5.0" version "5.5.0"
resolved "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz" resolved "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz"
@@ -9169,6 +9241,15 @@ portfinder@^1.0.26:
debug "^3.1.1" debug "^3.1.1"
mkdirp "^0.5.5" mkdirp "^0.5.5"
portfinder@^1.0.28:
version "1.0.32"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.32.tgz#2fe1b9e58389712429dc2bea5beb2146146c7f81"
integrity sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==
dependencies:
async "^2.6.4"
debug "^3.2.7"
mkdirp "^0.5.6"
posix-character-classes@^0.1.0: posix-character-classes@^0.1.0:
version "0.1.1" version "0.1.1"
resolved "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz" resolved "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz"
@@ -10063,6 +10144,13 @@ qs@6.7.0:
resolved "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz" resolved "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
qs@^6.4.0:
version "6.11.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==
dependencies:
side-channel "^1.0.4"
query-string@^4.1.0: query-string@^4.1.0:
version "4.3.4" version "4.3.4"
resolved "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz" resolved "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz"
@@ -10750,7 +10838,7 @@ safe-regex@^1.1.0:
dependencies: dependencies:
ret "~0.1.10" ret "~0.1.10"
"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.0: "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.1.0:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
@@ -10841,6 +10929,11 @@ schema-utils@^3.0.0:
ajv "^6.12.5" ajv "^6.12.5"
ajv-keywords "^3.5.2" ajv-keywords "^3.5.2"
secure-compare@3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/secure-compare/-/secure-compare-3.0.1.tgz#f1a0329b308b221fae37b9974f3d578d0ca999e3"
integrity sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==
select-hose@^2.0.0: select-hose@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz" resolved "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz"
@@ -12032,6 +12125,13 @@ union-value@^1.0.0:
is-extendable "^0.1.1" is-extendable "^0.1.1"
set-value "^2.0.1" set-value "^2.0.1"
union@~0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/union/-/union-0.5.0.tgz#b2c11be84f60538537b846edb9ba266ba0090075"
integrity sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==
dependencies:
qs "^6.4.0"
uniq@^1.0.1: uniq@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz" resolved "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz"
@@ -12108,6 +12208,11 @@ urix@^0.1.0:
resolved "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz" resolved "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz"
integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
url-join@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7"
integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==
url-loader@4.1.1: url-loader@4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz" resolved "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz"
@@ -12414,6 +12519,13 @@ whatwg-encoding@^1.0.5:
dependencies: dependencies:
iconv-lite "0.4.24" iconv-lite "0.4.24"
whatwg-encoding@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53"
integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==
dependencies:
iconv-lite "0.6.3"
whatwg-fetch@2.0.4: whatwg-fetch@2.0.4:
version "2.0.4" version "2.0.4"
resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz" resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz"