Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e68e11ff9f | |||
| b4750f4485 | |||
| 4426275184 | |||
| 2b4462c941 | |||
| 43b13d8e3a | |||
| 720f468f39 | |||
| 33300d19f6 | |||
| 5aed159991 | |||
| de1d221d1c | |||
| 9a68dbffe2 | |||
| 32d82219b1 | |||
| ba2c86fe1b | |||
| f1ae37c84b | |||
| ec350ba8b2 |
@@ -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, appState }) => (
|
PanelComponent: ({ updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
icon={load}
|
icon={load}
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ 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) => {
|
||||||
|
|||||||
+54
-12
@@ -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(activeTool) ||
|
hasBackground(appState.activeTool.type) ||
|
||||||
targetElements.some(
|
targetElements.some(
|
||||||
(element) =>
|
(element) =>
|
||||||
hasBackground(element.type) && !isTransparent(element.backgroundColor),
|
hasBackground(element.type) && !isTransparent(element.backgroundColor),
|
||||||
);
|
);
|
||||||
const showChangeBackgroundIcons =
|
const showChangeBackgroundIcons =
|
||||||
hasBackground(activeTool) ||
|
hasBackground(appState.activeTool.type) ||
|
||||||
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(activeTool) &&
|
{((hasStrokeColor(appState.activeTool.type) &&
|
||||||
activeTool !== "image" &&
|
appState.activeTool.type !== "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(activeTool) ||
|
{(hasStrokeWidth(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => hasStrokeWidth(element.type))) &&
|
targetElements.some((element) => hasStrokeWidth(element.type))) &&
|
||||||
renderAction("changeStrokeWidth")}
|
renderAction("changeStrokeWidth")}
|
||||||
|
|
||||||
{(activeTool === "freedraw" ||
|
{(appState.activeTool.type === "freedraw" ||
|
||||||
targetElements.some((element) => element.type === "freedraw")) &&
|
targetElements.some((element) => element.type === "freedraw")) &&
|
||||||
renderAction("changeStrokeShape")}
|
renderAction("changeStrokeShape")}
|
||||||
|
|
||||||
{(hasStrokeStyle(activeTool) ||
|
{(hasStrokeStyle(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => hasStrokeStyle(element.type))) && (
|
targetElements.some((element) => hasStrokeStyle(element.type))) && (
|
||||||
<>
|
<>
|
||||||
{renderAction("changeStrokeStyle")}
|
{renderAction("changeStrokeStyle")}
|
||||||
@@ -103,12 +103,12 @@ export const SelectedShapeActions = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(canChangeSharpness(activeTool) ||
|
{(canChangeSharpness(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => canChangeSharpness(element.type))) && (
|
targetElements.some((element) => canChangeSharpness(element.type))) && (
|
||||||
<>{renderAction("changeSharpness")}</>
|
<>{renderAction("changeSharpness")}</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(hasText(activeTool) ||
|
{(hasText(appState.activeTool.type) ||
|
||||||
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(activeTool) ||
|
{(canHaveArrowheads(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => canHaveArrowheads(element.type))) && (
|
targetElements.some((element) => canHaveArrowheads(element.type))) && (
|
||||||
<>{renderAction("changeArrowhead")}</>
|
<>{renderAction("changeArrowhead")}</>
|
||||||
)}
|
)}
|
||||||
@@ -271,3 +271,45 @@ 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>
|
||||||
|
);
|
||||||
|
|||||||
+87
-57
@@ -272,6 +272,7 @@ 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;
|
||||||
@@ -279,6 +280,22 @@ 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;
|
||||||
@@ -505,63 +522,69 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
value={this.excalidrawContainerValue}
|
value={this.excalidrawContainerValue}
|
||||||
>
|
>
|
||||||
<DeviceContext.Provider value={this.device}>
|
<DeviceContext.Provider value={this.device}>
|
||||||
<LayerUI
|
<ExcalidrawAppStateContext.Provider value={this.state}>
|
||||||
canvas={this.canvas}
|
<ExcalidrawElementsContext.Provider
|
||||||
appState={this.state}
|
value={this.scene.getNonDeletedElements()}
|
||||||
files={this.files}
|
>
|
||||||
setAppState={this.setAppState}
|
<LayerUI
|
||||||
actionManager={this.actionManager}
|
canvas={this.canvas}
|
||||||
elements={this.scene.getNonDeletedElements()}
|
appState={this.state}
|
||||||
onCollabButtonClick={onCollabButtonClick}
|
files={this.files}
|
||||||
onLockToggle={this.toggleLock}
|
setAppState={this.setAppState}
|
||||||
onPenModeToggle={this.togglePenMode}
|
actionManager={this.actionManager}
|
||||||
onInsertElements={(elements) =>
|
elements={this.scene.getNonDeletedElements()}
|
||||||
this.addElementsFromPasteOrLibrary({
|
onCollabButtonClick={onCollabButtonClick}
|
||||||
elements,
|
onLockToggle={this.toggleLock}
|
||||||
position: "center",
|
onPenModeToggle={this.togglePenMode}
|
||||||
files: null,
|
onInsertElements={(elements) =>
|
||||||
})
|
this.addElementsFromPasteOrLibrary({
|
||||||
}
|
elements,
|
||||||
langCode={getLanguage().code}
|
position: "center",
|
||||||
isCollaborating={this.props.isCollaborating}
|
files: null,
|
||||||
renderTopRightUI={renderTopRightUI}
|
})
|
||||||
renderCustomFooter={renderFooter}
|
}
|
||||||
renderCustomStats={renderCustomStats}
|
langCode={getLanguage().code}
|
||||||
showExitZenModeBtn={
|
isCollaborating={this.props.isCollaborating}
|
||||||
typeof this.props?.zenModeEnabled === "undefined" &&
|
renderTopRightUI={renderTopRightUI}
|
||||||
this.state.zenModeEnabled
|
renderCustomFooter={renderFooter}
|
||||||
}
|
renderCustomStats={renderCustomStats}
|
||||||
showThemeBtn={
|
showExitZenModeBtn={
|
||||||
typeof this.props?.theme === "undefined" &&
|
typeof this.props?.zenModeEnabled === "undefined" &&
|
||||||
this.props.UIOptions.canvasActions.theme
|
this.state.zenModeEnabled
|
||||||
}
|
}
|
||||||
libraryReturnUrl={this.props.libraryReturnUrl}
|
showThemeBtn={
|
||||||
UIOptions={this.props.UIOptions}
|
typeof this.props?.theme === "undefined" &&
|
||||||
focusContainer={this.focusContainer}
|
this.props.UIOptions.canvasActions.theme
|
||||||
library={this.library}
|
}
|
||||||
id={this.id}
|
libraryReturnUrl={this.props.libraryReturnUrl}
|
||||||
onImageAction={this.onImageAction}
|
UIOptions={this.props.UIOptions}
|
||||||
/>
|
focusContainer={this.focusContainer}
|
||||||
<div className="excalidraw-textEditorContainer" />
|
library={this.library}
|
||||||
<div className="excalidraw-contextMenuContainer" />
|
id={this.id}
|
||||||
{selectedElement.length === 1 && this.state.showHyperlinkPopup && (
|
onImageAction={this.onImageAction}
|
||||||
<Hyperlink
|
/>
|
||||||
key={selectedElement[0].id}
|
<div className="excalidraw-textEditorContainer" />
|
||||||
element={selectedElement[0]}
|
<div className="excalidraw-contextMenuContainer" />
|
||||||
appState={this.state}
|
{selectedElement.length === 1 &&
|
||||||
setAppState={this.setAppState}
|
this.state.showHyperlinkPopup && (
|
||||||
onLinkOpen={this.props.onLinkOpen}
|
<Hyperlink
|
||||||
/>
|
key={selectedElement[0].id}
|
||||||
)}
|
element={selectedElement[0]}
|
||||||
{this.state.toast !== null && (
|
setAppState={this.setAppState}
|
||||||
<Toast
|
onLinkOpen={this.props.onLinkOpen}
|
||||||
message={this.state.toast.message}
|
/>
|
||||||
onClose={() => this.setToast(null)}
|
)}
|
||||||
duration={this.state.toast.duration}
|
{this.state.toast !== null && (
|
||||||
closable={this.state.toast.closable}
|
<Toast
|
||||||
/>
|
message={this.state.toast.message}
|
||||||
)}
|
onClose={() => this.setToast(null)}
|
||||||
<main>{this.renderCanvas()}</main>
|
duration={this.state.toast.duration}
|
||||||
|
closable={this.state.toast.closable}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<main>{this.renderCanvas()}</main>
|
||||||
|
</ExcalidrawElementsContext.Provider>{" "}
|
||||||
|
</ExcalidrawAppStateContext.Provider>
|
||||||
</DeviceContext.Provider>
|
</DeviceContext.Provider>
|
||||||
</ExcalidrawContainerContext.Provider>
|
</ExcalidrawContainerContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
@@ -6143,6 +6166,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (width === 0 || height === 0) {
|
||||||
|
if (cb) {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
width,
|
width,
|
||||||
|
|||||||
@@ -343,6 +343,8 @@ const ColorInput = React.forwardRef(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ColorInput.displayName = "ColorInput";
|
||||||
|
|
||||||
export const ColorPicker = ({
|
export const ColorPicker = ({
|
||||||
type,
|
type,
|
||||||
color,
|
color,
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
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;
|
||||||
+75
-177
@@ -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, ZoomActions } from "./Actions";
|
import { SelectedShapeActions, ShapesSwitcher } 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 { actionToggleZenMode } from "../actions";
|
import Footer from "./Footer";
|
||||||
|
|
||||||
interface LayerUIProps {
|
interface LayerUIProps {
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
@@ -71,8 +71,8 @@ const LayerUI = ({
|
|||||||
appState,
|
appState,
|
||||||
files,
|
files,
|
||||||
setAppState,
|
setAppState,
|
||||||
canvas,
|
|
||||||
elements,
|
elements,
|
||||||
|
canvas,
|
||||||
onCollabButtonClick,
|
onCollabButtonClick,
|
||||||
onLockToggle,
|
onLockToggle,
|
||||||
onPenModeToggle,
|
onPenModeToggle,
|
||||||
@@ -210,8 +210,8 @@ const LayerUI = ({
|
|||||||
)}
|
)}
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
<BackgroundPickerAndDarkModeToggle
|
<BackgroundPickerAndDarkModeToggle
|
||||||
actionManager={actionManager}
|
|
||||||
appState={appState}
|
appState={appState}
|
||||||
|
actionManager={actionManager}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
showThemeBtn={showThemeBtn}
|
showThemeBtn={showThemeBtn}
|
||||||
/>
|
/>
|
||||||
@@ -244,7 +244,6 @@ const LayerUI = ({
|
|||||||
appState={appState}
|
appState={appState}
|
||||||
elements={elements}
|
elements={elements}
|
||||||
renderAction={actionManager.renderAction}
|
renderAction={actionManager.renderAction}
|
||||||
activeTool={appState.activeTool.type}
|
|
||||||
/>
|
/>
|
||||||
</Island>
|
</Island>
|
||||||
</Section>
|
</Section>
|
||||||
@@ -279,7 +278,6 @@ 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}
|
||||||
@@ -383,100 +381,7 @@ const LayerUI = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderBottomAppMenu = () => {
|
return (
|
||||||
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 && (
|
||||||
@@ -504,84 +409,77 @@ 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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
const renderStats = () => {
|
{!device.isMobile && (
|
||||||
if (!appState.showStats) {
|
<div
|
||||||
return null;
|
className={clsx("layer-ui__wrapper", {
|
||||||
}
|
"disable-pointerEvents":
|
||||||
return (
|
appState.draggingElement ||
|
||||||
<Stats
|
appState.resizingElement ||
|
||||||
appState={appState}
|
(appState.editingElement &&
|
||||||
setAppState={setAppState}
|
!isTextElement(appState.editingElement)),
|
||||||
elements={elements}
|
})}
|
||||||
onClose={() => {
|
style={
|
||||||
actionManager.executeAction(actionToggleStats);
|
appState.isLibraryOpen &&
|
||||||
}}
|
appState.isLibraryMenuDocked &&
|
||||||
renderCustomStats={renderCustomStats}
|
device.canDeviceFitSidebar
|
||||||
/>
|
? { width: `calc(100% - ${LIBRARY_SIDEBAR_WIDTH}px)` }
|
||||||
);
|
: {}
|
||||||
};
|
}
|
||||||
|
>
|
||||||
return device.isMobile ? (
|
{renderFixedSideContainer()}
|
||||||
<>
|
<Footer
|
||||||
{dialogs}
|
appState={appState}
|
||||||
<MobileMenu
|
actionManager={actionManager}
|
||||||
appState={appState}
|
renderCustomFooter={renderCustomFooter}
|
||||||
elements={elements}
|
showExitZenModeBtn={showExitZenModeBtn}
|
||||||
actionManager={actionManager}
|
/>
|
||||||
libraryMenu={libraryMenu}
|
{appState.showStats && (
|
||||||
renderJSONExportDialog={renderJSONExportDialog}
|
<Stats
|
||||||
renderImageExportDialog={renderImageExportDialog}
|
appState={appState}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
onCollabButtonClick={onCollabButtonClick}
|
elements={elements}
|
||||||
onLockToggle={() => onLockToggle()}
|
onClose={() => {
|
||||||
onPenModeToggle={onPenModeToggle}
|
actionManager.executeAction(actionToggleStats);
|
||||||
canvas={canvas}
|
}}
|
||||||
isCollaborating={isCollaborating}
|
renderCustomStats={renderCustomStats}
|
||||||
renderCustomFooter={renderCustomFooter}
|
/>
|
||||||
showThemeBtn={showThemeBtn}
|
)}
|
||||||
onImageAction={onImageAction}
|
{appState.scrolledOutside && (
|
||||||
renderTopRightUI={renderTopRightUI}
|
<button
|
||||||
renderStats={renderStats}
|
className="scroll-back-to-content"
|
||||||
/>
|
onClick={() => {
|
||||||
</>
|
setAppState({
|
||||||
) : (
|
...calculateScrollCenter(elements, appState, canvas),
|
||||||
<>
|
});
|
||||||
<div
|
}}
|
||||||
className={clsx("layer-ui__wrapper", {
|
>
|
||||||
"disable-pointerEvents":
|
{t("buttons.scrollBackToContent")}
|
||||||
appState.draggingElement ||
|
</button>
|
||||||
appState.resizingElement ||
|
)}
|
||||||
(appState.editingElement &&
|
</div>
|
||||||
!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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ export const LibraryMenu = ({
|
|||||||
onInsertLibraryItems,
|
onInsertLibraryItems,
|
||||||
pendingElements,
|
pendingElements,
|
||||||
onAddToLibrary,
|
onAddToLibrary,
|
||||||
theme,
|
|
||||||
setAppState,
|
setAppState,
|
||||||
files,
|
files,
|
||||||
libraryReturnUrl,
|
libraryReturnUrl,
|
||||||
@@ -93,7 +92,6 @@ 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"];
|
||||||
@@ -105,7 +103,6 @@ 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(
|
||||||
@@ -290,7 +287,7 @@ export const LibraryMenu = ({
|
|||||||
appState={appState}
|
appState={appState}
|
||||||
libraryReturnUrl={libraryReturnUrl}
|
libraryReturnUrl={libraryReturnUrl}
|
||||||
library={library}
|
library={library}
|
||||||
theme={theme}
|
theme={appState.theme}
|
||||||
files={files}
|
files={files}
|
||||||
id={id}
|
id={id}
|
||||||
selectedItems={selectedItems}
|
selectedItems={selectedItems}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { AppState } from "../types";
|
import { AppState, ExcalidrawProps } 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,6 +18,8 @@ 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;
|
||||||
@@ -42,7 +44,7 @@ type MobileMenuProps = {
|
|||||||
isMobile: boolean,
|
isMobile: boolean,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => JSX.Element | null;
|
) => JSX.Element | null;
|
||||||
renderStats: () => JSX.Element | null;
|
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MobileMenu = ({
|
export const MobileMenu = ({
|
||||||
@@ -62,7 +64,7 @@ export const MobileMenu = ({
|
|||||||
showThemeBtn,
|
showThemeBtn,
|
||||||
onImageAction,
|
onImageAction,
|
||||||
renderTopRightUI,
|
renderTopRightUI,
|
||||||
renderStats,
|
renderCustomStats,
|
||||||
}: MobileMenuProps) => {
|
}: MobileMenuProps) => {
|
||||||
const renderToolbar = () => {
|
const renderToolbar = () => {
|
||||||
return (
|
return (
|
||||||
@@ -184,7 +186,17 @@ export const MobileMenu = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!appState.viewModeEnabled && renderToolbar()}
|
{!appState.viewModeEnabled && renderToolbar()}
|
||||||
{renderStats()}
|
{!appState.openMenu && appState.showStats && (
|
||||||
|
<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={{
|
||||||
@@ -221,7 +233,6 @@ 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}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ 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";
|
||||||
@@ -16,13 +15,10 @@ 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}>
|
||||||
|
|||||||
@@ -187,3 +187,5 @@ ToolButton.defaultProps = {
|
|||||||
className: "",
|
className: "",
|
||||||
size: "medium",
|
size: "medium",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ToolButton.displayName = "ToolButton";
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ 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;
|
||||||
@@ -48,15 +49,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);
|
||||||
|
|||||||
+10
-3
@@ -107,12 +107,19 @@ 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -686,7 +686,9 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
const point = element.points[index];
|
const point = element.points[index];
|
||||||
const { x, y } = element;
|
const { x, y } = element;
|
||||||
return rotate(x + point[0], y + point[1], cx, cy, element.angle);
|
return point
|
||||||
|
? rotate(x + point[0], y + point[1], cx, cy, element.angle)
|
||||||
|
: rotate(x, y, cx, cy, element.angle);
|
||||||
}
|
}
|
||||||
|
|
||||||
static pointFromAbsoluteCoords(
|
static pointFromAbsoluteCoords(
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ 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;
|
||||||
@@ -20,6 +22,8 @@ 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>({
|
||||||
@@ -31,7 +35,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();
|
||||||
|
|||||||
@@ -666,10 +666,15 @@ 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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ 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)
|
||||||
|
|||||||
@@ -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> | <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> | <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) | <code>(url: string, token?: string) => void</code> | Imports library from given URL |
|
||||||
| [setToast](#setToast) | `({message: string, closable?:boolean, duration?:number} | null) => void` | This API can be used to show the toast with custom message. |
|
| [setToast](#setToast) | <code>({ message: string, closable?:boolean, duration?:number } | null) => void</code> | 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"] | "eraser" } | { 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"]| "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 |
|
| [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>)
|
loadLibraryFromBlob(blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>, defaultStatus: "published" | "unpublished")
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
This function loads the library from the blob.
|
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`.
|
||||||
|
|
||||||
#### `loadFromBlob`
|
#### `loadFromBlob`
|
||||||
|
|
||||||
|
|||||||
@@ -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(8);
|
expect(renderScene).toHaveBeenCalledTimes(7);
|
||||||
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(8);
|
expect(renderScene).toHaveBeenCalledTimes(7);
|
||||||
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(8);
|
expect(renderScene).toHaveBeenCalledTimes(7);
|
||||||
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(8);
|
expect(renderScene).toHaveBeenCalledTimes(7);
|
||||||
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(8);
|
expect(renderScene).toHaveBeenCalledTimes(7);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
|
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
|
|||||||
@@ -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(8);
|
expect(renderScene).toHaveBeenCalledTimes(7);
|
||||||
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(8);
|
expect(renderScene).toHaveBeenCalledTimes(7);
|
||||||
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();
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ describe("multi point mode in linear elements", () => {
|
|||||||
key: KEYS.ENTER,
|
key: KEYS.ENTER,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(14);
|
expect(renderScene).toHaveBeenCalledTimes(13);
|
||||||
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(14);
|
expect(renderScene).toHaveBeenCalledTimes(13);
|
||||||
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user