Merge remote-tracking branch 'origin/master' into feat-custom-actions
This commit is contained in:
commit
e385066b4b
@ -1,2 +1,2 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
yarn lint-staged
|
# yarn lint-staged
|
||||||
|
@ -283,15 +283,12 @@ const deviceContextInitialValue = {
|
|||||||
};
|
};
|
||||||
const DeviceContext = React.createContext<Device>(deviceContextInitialValue);
|
const DeviceContext = React.createContext<Device>(deviceContextInitialValue);
|
||||||
DeviceContext.displayName = "DeviceContext";
|
DeviceContext.displayName = "DeviceContext";
|
||||||
export const useDevice = () => useContext<Device>(DeviceContext);
|
|
||||||
|
|
||||||
export const ExcalidrawContainerContext = React.createContext<{
|
export const ExcalidrawContainerContext = React.createContext<{
|
||||||
container: HTMLDivElement | null;
|
container: HTMLDivElement | null;
|
||||||
id: string | null;
|
id: string | null;
|
||||||
}>({ container: null, id: null });
|
}>({ container: null, id: null });
|
||||||
ExcalidrawContainerContext.displayName = "ExcalidrawContainerContext";
|
ExcalidrawContainerContext.displayName = "ExcalidrawContainerContext";
|
||||||
export const useExcalidrawContainer = () =>
|
|
||||||
useContext(ExcalidrawContainerContext);
|
|
||||||
|
|
||||||
const ExcalidrawElementsContext = React.createContext<
|
const ExcalidrawElementsContext = React.createContext<
|
||||||
readonly NonDeletedExcalidrawElement[]
|
readonly NonDeletedExcalidrawElement[]
|
||||||
@ -309,7 +306,9 @@ ExcalidrawAppStateContext.displayName = "ExcalidrawAppStateContext";
|
|||||||
|
|
||||||
const ExcalidrawSetAppStateContext = React.createContext<
|
const ExcalidrawSetAppStateContext = React.createContext<
|
||||||
React.Component<any, AppState>["setState"]
|
React.Component<any, AppState>["setState"]
|
||||||
>(() => {});
|
>(() => {
|
||||||
|
console.warn("unitialized ExcalidrawSetAppStateContext context!");
|
||||||
|
});
|
||||||
ExcalidrawSetAppStateContext.displayName = "ExcalidrawSetAppStateContext";
|
ExcalidrawSetAppStateContext.displayName = "ExcalidrawSetAppStateContext";
|
||||||
|
|
||||||
const ExcalidrawActionManagerContext = React.createContext<ActionManager>(
|
const ExcalidrawActionManagerContext = React.createContext<ActionManager>(
|
||||||
@ -317,6 +316,9 @@ const ExcalidrawActionManagerContext = React.createContext<ActionManager>(
|
|||||||
);
|
);
|
||||||
ExcalidrawActionManagerContext.displayName = "ExcalidrawActionManagerContext";
|
ExcalidrawActionManagerContext.displayName = "ExcalidrawActionManagerContext";
|
||||||
|
|
||||||
|
export const useDevice = () => useContext<Device>(DeviceContext);
|
||||||
|
export const useExcalidrawContainer = () =>
|
||||||
|
useContext(ExcalidrawContainerContext);
|
||||||
export const useExcalidrawElements = () =>
|
export const useExcalidrawElements = () =>
|
||||||
useContext(ExcalidrawElementsContext);
|
useContext(ExcalidrawElementsContext);
|
||||||
export const useExcalidrawAppState = () =>
|
export const useExcalidrawAppState = () =>
|
||||||
@ -539,8 +541,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
this.state,
|
this.state,
|
||||||
);
|
);
|
||||||
const { onCollabButtonClick, renderTopRightUI, renderCustomStats } =
|
const { renderTopRightUI, renderCustomStats } = this.props;
|
||||||
this.props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -574,7 +575,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
setAppState={this.setAppState}
|
setAppState={this.setAppState}
|
||||||
actionManager={this.actionManager}
|
actionManager={this.actionManager}
|
||||||
elements={this.scene.getNonDeletedElements()}
|
elements={this.scene.getNonDeletedElements()}
|
||||||
onCollabButtonClick={onCollabButtonClick}
|
|
||||||
onLockToggle={this.toggleLock}
|
onLockToggle={this.toggleLock}
|
||||||
onPenModeToggle={this.togglePenMode}
|
onPenModeToggle={this.togglePenMode}
|
||||||
onInsertElements={(elements) =>
|
onInsertElements={(elements) =>
|
||||||
@ -601,6 +601,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
id={this.id}
|
id={this.id}
|
||||||
onImageAction={this.onImageAction}
|
onImageAction={this.onImageAction}
|
||||||
renderWelcomeScreen={
|
renderWelcomeScreen={
|
||||||
|
!this.state.isLoading &&
|
||||||
|
this.props.UIOptions.welcomeScreen &&
|
||||||
this.state.showWelcomeScreen &&
|
this.state.showWelcomeScreen &&
|
||||||
this.state.activeTool.type === "selection" &&
|
this.state.activeTool.type === "selection" &&
|
||||||
!this.scene.getElementsIncludingDeleted().length
|
!this.scene.getElementsIncludingDeleted().length
|
||||||
|
7
src/components/Button.scss
Normal file
7
src/components/Button.scss
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
@import "../css/theme";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.excalidraw-button {
|
||||||
|
@include outlineButtonStyles;
|
||||||
|
}
|
||||||
|
}
|
35
src/components/Button.tsx
Normal file
35
src/components/Button.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import "./Button.scss";
|
||||||
|
|
||||||
|
interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||||
|
type?: "button" | "submit" | "reset";
|
||||||
|
onSelect: () => any;
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A generic button component that follows Excalidraw's design system.
|
||||||
|
* Style can be customised using `className` or `style` prop.
|
||||||
|
* Accepts all props that a regular `button` element accepts.
|
||||||
|
*/
|
||||||
|
export const Button = ({
|
||||||
|
type = "button",
|
||||||
|
onSelect,
|
||||||
|
children,
|
||||||
|
className = "",
|
||||||
|
...rest
|
||||||
|
}: ButtonProps) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={(event) => {
|
||||||
|
onSelect();
|
||||||
|
rest.onClick?.(event);
|
||||||
|
}}
|
||||||
|
type={type}
|
||||||
|
className={`excalidraw-button ${className}`}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
@ -1,32 +0,0 @@
|
|||||||
import { t } from "../i18n";
|
|
||||||
import { UsersIcon } from "./icons";
|
|
||||||
|
|
||||||
import "./CollabButton.scss";
|
|
||||||
import clsx from "clsx";
|
|
||||||
|
|
||||||
const CollabButton = ({
|
|
||||||
isCollaborating,
|
|
||||||
collaboratorCount,
|
|
||||||
onClick,
|
|
||||||
}: {
|
|
||||||
isCollaborating: boolean;
|
|
||||||
collaboratorCount: number;
|
|
||||||
onClick: () => void;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className={clsx("collab-button", { active: isCollaborating })}
|
|
||||||
type="button"
|
|
||||||
onClick={onClick}
|
|
||||||
style={{ position: "relative" }}
|
|
||||||
title={t("labels.liveCollaboration")}
|
|
||||||
>
|
|
||||||
{UsersIcon}
|
|
||||||
{collaboratorCount > 0 && (
|
|
||||||
<div className="CollabButton-collaborators">{collaboratorCount}</div>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CollabButton;
|
|
@ -1,3 +1,5 @@
|
|||||||
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.FixedSideContainer {
|
.FixedSideContainer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -9,10 +11,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.FixedSideContainer_side_top {
|
.FixedSideContainer_side_top {
|
||||||
left: 1rem;
|
left: var(--editor-container-padding);
|
||||||
top: 1rem;
|
top: var(--editor-container-padding);
|
||||||
right: 1rem;
|
right: var(--editor-container-padding);
|
||||||
bottom: 1rem;
|
bottom: var(--editor-container-padding);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,10 +14,10 @@ import {
|
|||||||
ExcalidrawProps,
|
ExcalidrawProps,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
UIChildrenComponents,
|
UIChildrenComponents,
|
||||||
|
UIWelcomeScreenComponents,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { muteFSAbortError, ReactChildrenToObject } from "../utils";
|
import { isShallowEqual, muteFSAbortError, getReactChildren } from "../utils";
|
||||||
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
||||||
import CollabButton from "./CollabButton";
|
|
||||||
import { ErrorDialog } from "./ErrorDialog";
|
import { ErrorDialog } from "./ErrorDialog";
|
||||||
import { ExportCB, ImageExportDialog } from "./ImageExportDialog";
|
import { ExportCB, ImageExportDialog } from "./ImageExportDialog";
|
||||||
import { FixedSideContainer } from "./FixedSideContainer";
|
import { FixedSideContainer } from "./FixedSideContainer";
|
||||||
@ -45,13 +45,11 @@ 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/Footer";
|
import Footer from "./footer/Footer";
|
||||||
import { WelcomeScreenMenuArrow, WelcomeScreenTopToolbarArrow } from "./icons";
|
import WelcomeScreen from "./welcome-screen/WelcomeScreen";
|
||||||
import WelcomeScreen from "./WelcomeScreen";
|
|
||||||
import { hostSidebarCountersAtom } from "./Sidebar/Sidebar";
|
import { hostSidebarCountersAtom } from "./Sidebar/Sidebar";
|
||||||
import { jotaiScope } from "../jotai";
|
import { jotaiScope } from "../jotai";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import WelcomeScreenDecor from "./WelcomeScreenDecor";
|
import MainMenu from "./main-menu/MainMenu";
|
||||||
import MainMenu from "./mainMenu/MainMenu";
|
|
||||||
|
|
||||||
interface LayerUIProps {
|
interface LayerUIProps {
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
@ -60,7 +58,6 @@ interface LayerUIProps {
|
|||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
onCollabButtonClick?: () => void;
|
|
||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
onPenModeToggle: () => void;
|
onPenModeToggle: () => void;
|
||||||
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
||||||
@ -88,7 +85,6 @@ const LayerUI = ({
|
|||||||
setAppState,
|
setAppState,
|
||||||
elements,
|
elements,
|
||||||
canvas,
|
canvas,
|
||||||
onCollabButtonClick,
|
|
||||||
onLockToggle,
|
onLockToggle,
|
||||||
onPenModeToggle,
|
onPenModeToggle,
|
||||||
onInsertElements,
|
onInsertElements,
|
||||||
@ -109,8 +105,27 @@ const LayerUI = ({
|
|||||||
}: LayerUIProps) => {
|
}: LayerUIProps) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
|
|
||||||
const childrenComponents =
|
const [childrenComponents, restChildren] =
|
||||||
ReactChildrenToObject<UIChildrenComponents>(children);
|
getReactChildren<UIChildrenComponents>(children, {
|
||||||
|
Menu: true,
|
||||||
|
FooterCenter: true,
|
||||||
|
WelcomeScreen: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [WelcomeScreenComponents] = getReactChildren<UIWelcomeScreenComponents>(
|
||||||
|
renderWelcomeScreen
|
||||||
|
? (
|
||||||
|
childrenComponents?.WelcomeScreen ?? (
|
||||||
|
<WelcomeScreen>
|
||||||
|
<WelcomeScreen.Center />
|
||||||
|
<WelcomeScreen.Hints.MenuHint />
|
||||||
|
<WelcomeScreen.Hints.ToolbarHint />
|
||||||
|
<WelcomeScreen.Hints.HelpHint />
|
||||||
|
</WelcomeScreen>
|
||||||
|
)
|
||||||
|
)?.props?.children
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
|
||||||
const renderJSONExportDialog = () => {
|
const renderJSONExportDialog = () => {
|
||||||
if (!UIOptions.canvasActions.export) {
|
if (!UIOptions.canvasActions.export) {
|
||||||
@ -191,12 +206,6 @@ const LayerUI = ({
|
|||||||
{UIOptions.canvasActions.saveAsImage && (
|
{UIOptions.canvasActions.saveAsImage && (
|
||||||
<MainMenu.DefaultItems.SaveAsImage />
|
<MainMenu.DefaultItems.SaveAsImage />
|
||||||
)}
|
)}
|
||||||
{onCollabButtonClick && (
|
|
||||||
<MainMenu.DefaultItems.LiveCollaboration
|
|
||||||
onSelect={onCollabButtonClick}
|
|
||||||
isCollaborating={isCollaborating}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<MainMenu.DefaultItems.Help />
|
<MainMenu.DefaultItems.Help />
|
||||||
<MainMenu.DefaultItems.ClearCanvas />
|
<MainMenu.DefaultItems.ClearCanvas />
|
||||||
<MainMenu.Separator />
|
<MainMenu.Separator />
|
||||||
@ -212,15 +221,10 @@ const LayerUI = ({
|
|||||||
};
|
};
|
||||||
const renderCanvasActions = () => (
|
const renderCanvasActions = () => (
|
||||||
<div style={{ position: "relative" }}>
|
<div style={{ position: "relative" }}>
|
||||||
<WelcomeScreenDecor
|
{WelcomeScreenComponents.MenuHint}
|
||||||
shouldRender={renderWelcomeScreen && !appState.isLoading}
|
{/* wrapping to Fragment stops React from occasionally complaining
|
||||||
>
|
about identical Keys */}
|
||||||
<div className="virgil WelcomeScreen-decor WelcomeScreen-decor--menu-pointer">
|
<>{renderMenu()}</>
|
||||||
{WelcomeScreenMenuArrow}
|
|
||||||
<div>{t("welcomeScreen.menuHints")}</div>
|
|
||||||
</div>
|
|
||||||
</WelcomeScreenDecor>
|
|
||||||
{renderMenu()}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -257,9 +261,7 @@ const LayerUI = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FixedSideContainer side="top">
|
<FixedSideContainer side="top">
|
||||||
{renderWelcomeScreen && !appState.isLoading && (
|
{WelcomeScreenComponents.Center}
|
||||||
<WelcomeScreen appState={appState} actionManager={actionManager} />
|
|
||||||
)}
|
|
||||||
<div className="App-menu App-menu_top">
|
<div className="App-menu App-menu_top">
|
||||||
<Stack.Col
|
<Stack.Col
|
||||||
gap={6}
|
gap={6}
|
||||||
@ -274,17 +276,7 @@ const LayerUI = ({
|
|||||||
<Section heading="shapes" className="shapes-section">
|
<Section heading="shapes" className="shapes-section">
|
||||||
{(heading: React.ReactNode) => (
|
{(heading: React.ReactNode) => (
|
||||||
<div style={{ position: "relative" }}>
|
<div style={{ position: "relative" }}>
|
||||||
<WelcomeScreenDecor
|
{WelcomeScreenComponents.ToolbarHint}
|
||||||
shouldRender={renderWelcomeScreen && !appState.isLoading}
|
|
||||||
>
|
|
||||||
<div className="virgil WelcomeScreen-decor WelcomeScreen-decor--top-toolbar-pointer">
|
|
||||||
<div className="WelcomeScreen-decor--top-toolbar-pointer__label">
|
|
||||||
{t("welcomeScreen.toolbarHints")}
|
|
||||||
</div>
|
|
||||||
{WelcomeScreenTopToolbarArrow}
|
|
||||||
</div>
|
|
||||||
</WelcomeScreenDecor>
|
|
||||||
|
|
||||||
<Stack.Col gap={4} align="start">
|
<Stack.Col gap={4} align="start">
|
||||||
<Stack.Row
|
<Stack.Row
|
||||||
gap={1}
|
gap={1}
|
||||||
@ -353,13 +345,6 @@ const LayerUI = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<UserList collaborators={appState.collaborators} />
|
<UserList collaborators={appState.collaborators} />
|
||||||
{onCollabButtonClick && (
|
|
||||||
<CollabButton
|
|
||||||
isCollaborating={isCollaborating}
|
|
||||||
collaboratorCount={appState.collaborators.size}
|
|
||||||
onClick={onCollabButtonClick}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{renderTopRightUI?.(device.isMobile, appState)}
|
{renderTopRightUI?.(device.isMobile, appState)}
|
||||||
{!appState.viewModeEnabled && (
|
{!appState.viewModeEnabled && (
|
||||||
<LibraryButton appState={appState} setAppState={setAppState} />
|
<LibraryButton appState={appState} setAppState={setAppState} />
|
||||||
@ -389,6 +374,7 @@ const LayerUI = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{restChildren}
|
||||||
{appState.isLoading && <LoadingMessage delay={250} />}
|
{appState.isLoading && <LoadingMessage delay={250} />}
|
||||||
{appState.errorMessage && (
|
{appState.errorMessage && (
|
||||||
<ErrorDialog
|
<ErrorDialog
|
||||||
@ -419,18 +405,15 @@ const LayerUI = ({
|
|||||||
)}
|
)}
|
||||||
{device.isMobile && (
|
{device.isMobile && (
|
||||||
<MobileMenu
|
<MobileMenu
|
||||||
renderWelcomeScreen={renderWelcomeScreen}
|
|
||||||
appState={appState}
|
appState={appState}
|
||||||
elements={elements}
|
elements={elements}
|
||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
renderJSONExportDialog={renderJSONExportDialog}
|
renderJSONExportDialog={renderJSONExportDialog}
|
||||||
renderImageExportDialog={renderImageExportDialog}
|
renderImageExportDialog={renderImageExportDialog}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
onCollabButtonClick={onCollabButtonClick}
|
|
||||||
onLockToggle={() => onLockToggle()}
|
onLockToggle={() => onLockToggle()}
|
||||||
onPenModeToggle={onPenModeToggle}
|
onPenModeToggle={onPenModeToggle}
|
||||||
canvas={canvas}
|
canvas={canvas}
|
||||||
isCollaborating={isCollaborating}
|
|
||||||
onImageAction={onImageAction}
|
onImageAction={onImageAction}
|
||||||
renderTopRightUI={renderTopRightUI}
|
renderTopRightUI={renderTopRightUI}
|
||||||
renderCustomStats={renderCustomStats}
|
renderCustomStats={renderCustomStats}
|
||||||
@ -438,6 +421,7 @@ const LayerUI = ({
|
|||||||
device={device}
|
device={device}
|
||||||
renderMenu={renderMenu}
|
renderMenu={renderMenu}
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
|
welcomeScreenCenter={WelcomeScreenComponents.Center}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -462,13 +446,12 @@ const LayerUI = ({
|
|||||||
>
|
>
|
||||||
{renderFixedSideContainer()}
|
{renderFixedSideContainer()}
|
||||||
<Footer
|
<Footer
|
||||||
renderWelcomeScreen={renderWelcomeScreen}
|
|
||||||
appState={appState}
|
appState={appState}
|
||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
showExitZenModeBtn={showExitZenModeBtn}
|
showExitZenModeBtn={showExitZenModeBtn}
|
||||||
footerCenter={childrenComponents.FooterCenter}
|
footerCenter={childrenComponents.FooterCenter}
|
||||||
|
welcomeScreenHelp={WelcomeScreenComponents.HelpHint}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{appState.showStats && (
|
{appState.showStats && (
|
||||||
<Stats
|
<Stats
|
||||||
appState={appState}
|
appState={appState}
|
||||||
@ -500,28 +483,39 @@ const LayerUI = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const areEqual = (prev: LayerUIProps, next: LayerUIProps) => {
|
const stripIrrelevantAppStateProps = (
|
||||||
const getNecessaryObj = (appState: AppState): Partial<AppState> => {
|
appState: AppState,
|
||||||
const {
|
): Partial<AppState> => {
|
||||||
suggestedBindings,
|
const { suggestedBindings, startBoundElement, cursorButton, ...ret } =
|
||||||
startBoundElement: boundElement,
|
appState;
|
||||||
...ret
|
|
||||||
} = appState;
|
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
const prevAppState = getNecessaryObj(prev.appState);
|
|
||||||
const nextAppState = getNecessaryObj(next.appState);
|
|
||||||
|
|
||||||
const keys = Object.keys(prevAppState) as (keyof Partial<AppState>)[];
|
const areEqual = (prevProps: LayerUIProps, nextProps: LayerUIProps) => {
|
||||||
|
// short-circuit early
|
||||||
|
if (prevProps.children !== nextProps.children) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
canvas: _prevCanvas,
|
||||||
|
// not stable, but shouldn't matter in our case
|
||||||
|
onInsertElements: _prevOnInsertElements,
|
||||||
|
appState: prevAppState,
|
||||||
|
...prev
|
||||||
|
} = prevProps;
|
||||||
|
const {
|
||||||
|
canvas: _nextCanvas,
|
||||||
|
onInsertElements: _nextOnInsertElements,
|
||||||
|
appState: nextAppState,
|
||||||
|
...next
|
||||||
|
} = nextProps;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
prev.renderTopRightUI === next.renderTopRightUI &&
|
isShallowEqual(
|
||||||
prev.renderCustomStats === next.renderCustomStats &&
|
stripIrrelevantAppStateProps(prevAppState),
|
||||||
prev.renderCustomSidebar === next.renderCustomSidebar &&
|
stripIrrelevantAppStateProps(nextAppState),
|
||||||
prev.langCode === next.langCode &&
|
) && isShallowEqual(prev, next)
|
||||||
prev.elements === next.elements &&
|
|
||||||
prev.files === next.files &&
|
|
||||||
keys.every((key) => prevAppState[key] === nextAppState[key])
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -193,7 +193,7 @@ export const LibraryMenuHeader: React.FC<{
|
|||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={onLibraryImport}
|
onSelect={onLibraryImport}
|
||||||
icon={LoadIcon}
|
icon={LoadIcon}
|
||||||
dataTestId="lib-dropdown--load"
|
data-testid="lib-dropdown--load"
|
||||||
>
|
>
|
||||||
{t("buttons.load")}
|
{t("buttons.load")}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
@ -202,7 +202,7 @@ export const LibraryMenuHeader: React.FC<{
|
|||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={onLibraryExport}
|
onSelect={onLibraryExport}
|
||||||
icon={ExportIcon}
|
icon={ExportIcon}
|
||||||
dataTestId="lib-dropdown--export"
|
data-testid="lib-dropdown--export"
|
||||||
>
|
>
|
||||||
{t("buttons.export")}
|
{t("buttons.export")}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
@ -219,7 +219,7 @@ export const LibraryMenuHeader: React.FC<{
|
|||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
icon={publishIcon}
|
icon={publishIcon}
|
||||||
onSelect={() => setShowPublishLibraryDialog(true)}
|
onSelect={() => setShowPublishLibraryDialog(true)}
|
||||||
dataTestId="lib-dropdown--remove"
|
data-testid="lib-dropdown--remove"
|
||||||
>
|
>
|
||||||
{t("buttons.publishLibrary")}
|
{t("buttons.publishLibrary")}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { AppState, Device, ExcalidrawProps } from "../types";
|
import {
|
||||||
|
AppState,
|
||||||
|
Device,
|
||||||
|
ExcalidrawProps,
|
||||||
|
UIWelcomeScreenComponents,
|
||||||
|
} 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";
|
||||||
@ -17,7 +22,6 @@ import { LibraryButton } from "./LibraryButton";
|
|||||||
import { PenModeButton } from "./PenModeButton";
|
import { PenModeButton } from "./PenModeButton";
|
||||||
import { Stats } from "./Stats";
|
import { Stats } from "./Stats";
|
||||||
import { actionToggleStats } from "../actions";
|
import { actionToggleStats } from "../actions";
|
||||||
import WelcomeScreen from "./WelcomeScreen";
|
|
||||||
|
|
||||||
type MobileMenuProps = {
|
type MobileMenuProps = {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
@ -26,11 +30,9 @@ type MobileMenuProps = {
|
|||||||
renderImageExportDialog: () => React.ReactNode;
|
renderImageExportDialog: () => React.ReactNode;
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
onCollabButtonClick?: () => void;
|
|
||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
onPenModeToggle: () => void;
|
onPenModeToggle: () => void;
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
isCollaborating: boolean;
|
|
||||||
|
|
||||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||||
renderTopRightUI?: (
|
renderTopRightUI?: (
|
||||||
@ -40,9 +42,9 @@ type MobileMenuProps = {
|
|||||||
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||||
renderSidebars: () => JSX.Element | null;
|
renderSidebars: () => JSX.Element | null;
|
||||||
device: Device;
|
device: Device;
|
||||||
renderWelcomeScreen?: boolean;
|
|
||||||
renderMenu: () => React.ReactNode;
|
renderMenu: () => React.ReactNode;
|
||||||
onContextMenu?: (event: React.MouseEvent, source: string) => void;
|
onContextMenu?: (event: React.MouseEvent, source: string) => void;
|
||||||
|
welcomeScreenCenter: UIWelcomeScreenComponents["Center"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MobileMenu = ({
|
export const MobileMenu = ({
|
||||||
@ -53,22 +55,19 @@ export const MobileMenu = ({
|
|||||||
onLockToggle,
|
onLockToggle,
|
||||||
onPenModeToggle,
|
onPenModeToggle,
|
||||||
canvas,
|
canvas,
|
||||||
isCollaborating,
|
|
||||||
onImageAction,
|
onImageAction,
|
||||||
renderTopRightUI,
|
renderTopRightUI,
|
||||||
renderCustomStats,
|
renderCustomStats,
|
||||||
renderSidebars,
|
renderSidebars,
|
||||||
device,
|
device,
|
||||||
renderWelcomeScreen,
|
|
||||||
renderMenu,
|
renderMenu,
|
||||||
onContextMenu,
|
onContextMenu,
|
||||||
|
welcomeScreenCenter,
|
||||||
}: MobileMenuProps) => {
|
}: MobileMenuProps) => {
|
||||||
const renderToolbar = () => {
|
const renderToolbar = () => {
|
||||||
return (
|
return (
|
||||||
<FixedSideContainer side="top" className="App-top-bar">
|
<FixedSideContainer side="top" className="App-top-bar">
|
||||||
{renderWelcomeScreen && !appState.isLoading && (
|
{welcomeScreenCenter}
|
||||||
<WelcomeScreen appState={appState} actionManager={actionManager} />
|
|
||||||
)}
|
|
||||||
<Section heading="shapes">
|
<Section heading="shapes">
|
||||||
{(heading: React.ReactNode) => (
|
{(heading: React.ReactNode) => (
|
||||||
<Stack.Col gap={4} align="center">
|
<Stack.Col gap={4} align="center">
|
||||||
@ -76,20 +75,6 @@ export const MobileMenu = ({
|
|||||||
<Island padding={1} className="App-toolbar App-toolbar--mobile">
|
<Island padding={1} className="App-toolbar App-toolbar--mobile">
|
||||||
{heading}
|
{heading}
|
||||||
<Stack.Row gap={1}>
|
<Stack.Row gap={1}>
|
||||||
{/* <PenModeButton
|
|
||||||
checked={appState.penMode}
|
|
||||||
onChange={onPenModeToggle}
|
|
||||||
title={t("toolBar.penMode")}
|
|
||||||
isMobile
|
|
||||||
penDetected={appState.penDetected}
|
|
||||||
/>
|
|
||||||
<LockButton
|
|
||||||
checked={appState.activeTool.locked}
|
|
||||||
onChange={onLockToggle}
|
|
||||||
title={t("toolBar.lock")}
|
|
||||||
isMobile
|
|
||||||
/>
|
|
||||||
<div className="App-toolbar__divider"></div> */}
|
|
||||||
<ShapesSwitcher
|
<ShapesSwitcher
|
||||||
appState={appState}
|
appState={appState}
|
||||||
canvas={canvas}
|
canvas={canvas}
|
||||||
@ -112,7 +97,6 @@ export const MobileMenu = ({
|
|||||||
title={t("toolBar.penMode")}
|
title={t("toolBar.penMode")}
|
||||||
isMobile
|
isMobile
|
||||||
penDetected={appState.penDetected}
|
penDetected={appState.penDetected}
|
||||||
// penDetected={true}
|
|
||||||
/>
|
/>
|
||||||
<LockButton
|
<LockButton
|
||||||
checked={appState.activeTool.locked}
|
checked={appState.activeTool.locked}
|
||||||
|
@ -1,121 +0,0 @@
|
|||||||
import { actionLoadScene, actionShortcuts } from "../actions";
|
|
||||||
import { ActionManager } from "../actions/manager";
|
|
||||||
import { getShortcutFromShortcutName } from "../actions/shortcuts";
|
|
||||||
import { isExcalidrawPlusSignedUser } from "../constants";
|
|
||||||
import { t } from "../i18n";
|
|
||||||
import { AppState } from "../types";
|
|
||||||
import { ExcalLogo, HelpIcon, LoadIcon, PlusPromoIcon } from "./icons";
|
|
||||||
import "./WelcomeScreen.scss";
|
|
||||||
|
|
||||||
const WelcomeScreenItem = ({
|
|
||||||
label,
|
|
||||||
shortcut,
|
|
||||||
onClick,
|
|
||||||
icon,
|
|
||||||
link,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
shortcut: string | null;
|
|
||||||
onClick?: () => void;
|
|
||||||
icon: JSX.Element;
|
|
||||||
link?: string;
|
|
||||||
}) => {
|
|
||||||
if (link) {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
className="WelcomeScreen-item"
|
|
||||||
href={link}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<div className="WelcomeScreen-item__label">
|
|
||||||
{icon}
|
|
||||||
{label}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button className="WelcomeScreen-item" type="button" onClick={onClick}>
|
|
||||||
<div className="WelcomeScreen-item__label">
|
|
||||||
{icon}
|
|
||||||
{label}
|
|
||||||
</div>
|
|
||||||
{shortcut && (
|
|
||||||
<div className="WelcomeScreen-item__shortcut">{shortcut}</div>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const WelcomeScreen = ({
|
|
||||||
appState,
|
|
||||||
actionManager,
|
|
||||||
}: {
|
|
||||||
appState: AppState;
|
|
||||||
actionManager: ActionManager;
|
|
||||||
}) => {
|
|
||||||
let subheadingJSX;
|
|
||||||
|
|
||||||
if (isExcalidrawPlusSignedUser) {
|
|
||||||
subheadingJSX = t("welcomeScreen.switchToPlusApp")
|
|
||||||
.split(/(Excalidraw\+)/)
|
|
||||||
.map((bit, idx) => {
|
|
||||||
if (bit === "Excalidraw+") {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
style={{ pointerEvents: "all" }}
|
|
||||||
href={`${process.env.REACT_APP_PLUS_APP}?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`}
|
|
||||||
key={idx}
|
|
||||||
>
|
|
||||||
Excalidraw+
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return bit;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
subheadingJSX = t("welcomeScreen.data");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="WelcomeScreen-container">
|
|
||||||
<div className="WelcomeScreen-logo virgil WelcomeScreen-decor">
|
|
||||||
{ExcalLogo} Excalidraw
|
|
||||||
</div>
|
|
||||||
<div className="virgil WelcomeScreen-decor WelcomeScreen-decor--subheading">
|
|
||||||
{subheadingJSX}
|
|
||||||
</div>
|
|
||||||
<div className="WelcomeScreen-items">
|
|
||||||
{!appState.viewModeEnabled && (
|
|
||||||
<WelcomeScreenItem
|
|
||||||
// TODO barnabasmolnar/editor-redesign
|
|
||||||
// do we want the internationalized labels here that are currently
|
|
||||||
// in use elsewhere or new ones?
|
|
||||||
label={t("buttons.load")}
|
|
||||||
onClick={() => actionManager.executeAction(actionLoadScene)}
|
|
||||||
shortcut={getShortcutFromShortcutName("loadScene")}
|
|
||||||
icon={LoadIcon}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<WelcomeScreenItem
|
|
||||||
onClick={() => actionManager.executeAction(actionShortcuts)}
|
|
||||||
label={t("helpDialog.title")}
|
|
||||||
shortcut="?"
|
|
||||||
icon={HelpIcon}
|
|
||||||
/>
|
|
||||||
{!isExcalidrawPlusSignedUser && (
|
|
||||||
<WelcomeScreenItem
|
|
||||||
link="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
|
|
||||||
label="Try Excalidraw Plus!"
|
|
||||||
shortcut={null}
|
|
||||||
icon={PlusPromoIcon}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default WelcomeScreen;
|
|
@ -1,11 +0,0 @@
|
|||||||
import { ReactNode } from "react";
|
|
||||||
|
|
||||||
const WelcomeScreenDecor = ({
|
|
||||||
children,
|
|
||||||
shouldRender,
|
|
||||||
}: {
|
|
||||||
children: ReactNode;
|
|
||||||
shouldRender: boolean;
|
|
||||||
}) => (shouldRender ? <>{children}</> : null);
|
|
||||||
|
|
||||||
export default WelcomeScreenDecor;
|
|
@ -73,7 +73,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--button-hover);
|
background-color: var(--button-hover-bg);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,30 +9,23 @@ const DropdownMenuItem = ({
|
|||||||
icon,
|
icon,
|
||||||
onSelect,
|
onSelect,
|
||||||
children,
|
children,
|
||||||
dataTestId,
|
|
||||||
shortcut,
|
shortcut,
|
||||||
className,
|
className,
|
||||||
style,
|
...rest
|
||||||
ariaLabel,
|
|
||||||
}: {
|
}: {
|
||||||
icon?: JSX.Element;
|
icon?: JSX.Element;
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
dataTestId?: string;
|
|
||||||
shortcut?: string;
|
shortcut?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: React.CSSProperties;
|
} & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||||
ariaLabel?: string;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
aria-label={ariaLabel}
|
{...rest}
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
data-testid={dataTestId}
|
|
||||||
title={ariaLabel}
|
|
||||||
type="button"
|
type="button"
|
||||||
className={getDrodownMenuItemClassName(className)}
|
className={getDrodownMenuItemClassName(className)}
|
||||||
style={style}
|
title={rest.title ?? rest["aria-label"]}
|
||||||
>
|
>
|
||||||
<MenuItemContent icon={icon} shortcut={shortcut}>
|
<MenuItemContent icon={icon} shortcut={shortcut}>
|
||||||
{children}
|
{children}
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
const DropdownMenuItemCustom = ({
|
const DropdownMenuItemCustom = ({
|
||||||
children,
|
children,
|
||||||
className = "",
|
className = "",
|
||||||
style,
|
...rest
|
||||||
dataTestId,
|
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: React.CSSProperties;
|
} & React.HTMLAttributes<HTMLDivElement>) => {
|
||||||
dataTestId?: string;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
{...rest}
|
||||||
className={`dropdown-menu-item-base dropdown-menu-item-custom ${className}`.trim()}
|
className={`dropdown-menu-item-base dropdown-menu-item-custom ${className}`.trim()}
|
||||||
style={style}
|
|
||||||
data-testid={dataTestId}
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,33 +3,26 @@ import React from "react";
|
|||||||
import { getDrodownMenuItemClassName } from "./DropdownMenuItem";
|
import { getDrodownMenuItemClassName } from "./DropdownMenuItem";
|
||||||
const DropdownMenuItemLink = ({
|
const DropdownMenuItemLink = ({
|
||||||
icon,
|
icon,
|
||||||
dataTestId,
|
|
||||||
shortcut,
|
shortcut,
|
||||||
href,
|
href,
|
||||||
children,
|
children,
|
||||||
className = "",
|
className = "",
|
||||||
style,
|
...rest
|
||||||
ariaLabel,
|
|
||||||
}: {
|
}: {
|
||||||
icon?: JSX.Element;
|
icon?: JSX.Element;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
dataTestId?: string;
|
|
||||||
shortcut?: string;
|
shortcut?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
href: string;
|
href: string;
|
||||||
style?: React.CSSProperties;
|
} & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||||
ariaLabel?: string;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
|
{...rest}
|
||||||
href={href}
|
href={href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className={getDrodownMenuItemClassName(className)}
|
className={getDrodownMenuItemClassName(className)}
|
||||||
style={style}
|
title={rest.title ?? rest["aria-label"]}
|
||||||
data-testid={dataTestId}
|
|
||||||
title={ariaLabel}
|
|
||||||
aria-label={ariaLabel}
|
|
||||||
>
|
>
|
||||||
<MenuItemContent icon={icon} shortcut={shortcut}>
|
<MenuItemContent icon={icon} shortcut={shortcut}>
|
||||||
{children}
|
{children}
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { actionShortcuts } from "../../actions";
|
import { actionShortcuts } from "../../actions";
|
||||||
import { ActionManager } from "../../actions/manager";
|
import { ActionManager } from "../../actions/manager";
|
||||||
import { t } from "../../i18n";
|
import {
|
||||||
import { AppState, UIChildrenComponents } from "../../types";
|
AppState,
|
||||||
|
UIChildrenComponents,
|
||||||
|
UIWelcomeScreenComponents,
|
||||||
|
} from "../../types";
|
||||||
import {
|
import {
|
||||||
ExitZenModeAction,
|
ExitZenModeAction,
|
||||||
FinalizeAction,
|
FinalizeAction,
|
||||||
@ -11,23 +14,21 @@ import {
|
|||||||
} from "../Actions";
|
} from "../Actions";
|
||||||
import { useDevice } from "../App";
|
import { useDevice } from "../App";
|
||||||
import { HelpButton } from "../HelpButton";
|
import { HelpButton } from "../HelpButton";
|
||||||
import { WelcomeScreenHelpArrow } from "../icons";
|
|
||||||
import { Section } from "../Section";
|
import { Section } from "../Section";
|
||||||
import Stack from "../Stack";
|
import Stack from "../Stack";
|
||||||
import WelcomeScreenDecor from "../WelcomeScreenDecor";
|
|
||||||
|
|
||||||
const Footer = ({
|
const Footer = ({
|
||||||
appState,
|
appState,
|
||||||
actionManager,
|
actionManager,
|
||||||
showExitZenModeBtn,
|
showExitZenModeBtn,
|
||||||
renderWelcomeScreen,
|
|
||||||
footerCenter,
|
footerCenter,
|
||||||
|
welcomeScreenHelp,
|
||||||
}: {
|
}: {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
showExitZenModeBtn: boolean;
|
showExitZenModeBtn: boolean;
|
||||||
renderWelcomeScreen: boolean;
|
|
||||||
footerCenter: UIChildrenComponents["FooterCenter"];
|
footerCenter: UIChildrenComponents["FooterCenter"];
|
||||||
|
welcomeScreenHelp: UIWelcomeScreenComponents["HelpHint"];
|
||||||
}) => {
|
}) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
const showFinalize =
|
const showFinalize =
|
||||||
@ -79,17 +80,8 @@ const Footer = ({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div style={{ position: "relative" }}>
|
<div style={{ position: "relative" }}>
|
||||||
<WelcomeScreenDecor
|
{welcomeScreenHelp}
|
||||||
shouldRender={renderWelcomeScreen && !appState.isLoading}
|
|
||||||
>
|
|
||||||
<div className="virgil WelcomeScreen-decor WelcomeScreen-decor--help-pointer">
|
|
||||||
<div>{t("welcomeScreen.helpHints")}</div>
|
|
||||||
{WelcomeScreenHelpArrow}
|
|
||||||
</div>
|
|
||||||
</WelcomeScreenDecor>
|
|
||||||
|
|
||||||
<HelpButton
|
<HelpButton
|
||||||
title={t("helpDialog.title")}
|
|
||||||
onClick={() => actionManager.executeAction(actionShortcuts)}
|
onClick={() => actionManager.executeAction(actionShortcuts)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -883,7 +883,7 @@ export const CenterHorizontallyIcon = createIcon(
|
|||||||
modifiedTablerIconProps,
|
modifiedTablerIconProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const UsersIcon = createIcon(
|
export const usersIcon = createIcon(
|
||||||
<g strokeWidth="1.5">
|
<g strokeWidth="1.5">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
<circle cx="9" cy="7" r="4"></circle>
|
<circle cx="9" cy="7" r="4"></circle>
|
||||||
|
@ -1,30 +1,23 @@
|
|||||||
@import "../css/variables.module";
|
@import "../../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.collab-button {
|
.collab-button {
|
||||||
@include outlineButtonStyles;
|
--button-bg: var(--color-primary);
|
||||||
width: var(--lg-button-size);
|
--button-color: white;
|
||||||
height: var(--lg-button-size);
|
--button-border: var(--color-primary);
|
||||||
|
|
||||||
|
--button-width: var(--lg-button-size);
|
||||||
|
--button-height: var(--lg-button-size);
|
||||||
|
|
||||||
|
--button-hover-bg: var(--color-primary-darker);
|
||||||
|
--button-hover-border: var(--color-primary-darker);
|
||||||
|
|
||||||
|
--button-active-bg: var(--color-primary-darker);
|
||||||
|
|
||||||
svg {
|
|
||||||
width: var(--lg-icon-size);
|
|
||||||
height: var(--lg-icon-size);
|
|
||||||
}
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
color: white;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
&:hover {
|
// double .active to force specificity
|
||||||
background-color: var(--color-primary-darker);
|
&.active.active {
|
||||||
border-color: var(--color-primary-darker);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: var(--color-primary-darker);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: #0fb884;
|
background-color: #0fb884;
|
||||||
border-color: #0fb884;
|
border-color: #0fb884;
|
||||||
|
|
@ -0,0 +1,40 @@
|
|||||||
|
import { t } from "../../i18n";
|
||||||
|
import { usersIcon } from "../icons";
|
||||||
|
import { Button } from "../Button";
|
||||||
|
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { useExcalidrawAppState } from "../App";
|
||||||
|
|
||||||
|
import "./LiveCollaborationTrigger.scss";
|
||||||
|
|
||||||
|
const LiveCollaborationTrigger = ({
|
||||||
|
isCollaborating,
|
||||||
|
onSelect,
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
isCollaborating: boolean;
|
||||||
|
onSelect: () => void;
|
||||||
|
} & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||||
|
const appState = useExcalidrawAppState();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
{...rest}
|
||||||
|
className={clsx("collab-button", { active: isCollaborating })}
|
||||||
|
type="button"
|
||||||
|
onSelect={onSelect}
|
||||||
|
style={{ position: "relative" }}
|
||||||
|
title={t("labels.liveCollaboration")}
|
||||||
|
>
|
||||||
|
{usersIcon}
|
||||||
|
{appState.collaborators.size > 0 && (
|
||||||
|
<div className="CollabButton-collaborators">
|
||||||
|
{appState.collaborators.size}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LiveCollaborationTrigger;
|
||||||
|
LiveCollaborationTrigger.displayName = "LiveCollaborationTrigger";
|
@ -1,4 +1,3 @@
|
|||||||
import clsx from "clsx";
|
|
||||||
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import {
|
import {
|
||||||
@ -15,7 +14,7 @@ import {
|
|||||||
save,
|
save,
|
||||||
SunIcon,
|
SunIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
UsersIcon,
|
usersIcon,
|
||||||
} from "../icons";
|
} from "../icons";
|
||||||
import { GithubIcon, DiscordIcon, TwitterIcon } from "../icons";
|
import { GithubIcon, DiscordIcon, TwitterIcon } from "../icons";
|
||||||
import DropdownMenuItem from "../dropdownMenu/DropdownMenuItem";
|
import DropdownMenuItem from "../dropdownMenu/DropdownMenuItem";
|
||||||
@ -31,6 +30,7 @@ import {
|
|||||||
import "./DefaultItems.scss";
|
import "./DefaultItems.scss";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import ConfirmDialog from "../ConfirmDialog";
|
import ConfirmDialog from "../ConfirmDialog";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
export const LoadScene = () => {
|
export const LoadScene = () => {
|
||||||
// FIXME Hack until we tie "t" to lang state
|
// FIXME Hack until we tie "t" to lang state
|
||||||
@ -46,9 +46,9 @@ export const LoadScene = () => {
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
icon={LoadIcon}
|
icon={LoadIcon}
|
||||||
onSelect={() => actionManager.executeAction(actionLoadScene)}
|
onSelect={() => actionManager.executeAction(actionLoadScene)}
|
||||||
dataTestId="load-button"
|
data-testid="load-button"
|
||||||
shortcut={getShortcutFromShortcutName("loadScene")}
|
shortcut={getShortcutFromShortcutName("loadScene")}
|
||||||
ariaLabel={t("buttons.load")}
|
aria-label={t("buttons.load")}
|
||||||
>
|
>
|
||||||
{t("buttons.load")}
|
{t("buttons.load")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@ -69,10 +69,10 @@ export const SaveToActiveFile = () => {
|
|||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
shortcut={getShortcutFromShortcutName("saveScene")}
|
shortcut={getShortcutFromShortcutName("saveScene")}
|
||||||
dataTestId="save-button"
|
data-testid="save-button"
|
||||||
onSelect={() => actionManager.executeAction(actionSaveToActiveFile)}
|
onSelect={() => actionManager.executeAction(actionSaveToActiveFile)}
|
||||||
icon={save}
|
icon={save}
|
||||||
ariaLabel={`${t("buttons.save")}`}
|
aria-label={`${t("buttons.save")}`}
|
||||||
>{`${t("buttons.save")}`}</DropdownMenuItem>
|
>{`${t("buttons.save")}`}</DropdownMenuItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -86,10 +86,10 @@ export const SaveAsImage = () => {
|
|||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
icon={ExportImageIcon}
|
icon={ExportImageIcon}
|
||||||
dataTestId="image-export-button"
|
data-testid="image-export-button"
|
||||||
onSelect={() => setAppState({ openDialog: "imageExport" })}
|
onSelect={() => setAppState({ openDialog: "imageExport" })}
|
||||||
shortcut={getShortcutFromShortcutName("imageExport")}
|
shortcut={getShortcutFromShortcutName("imageExport")}
|
||||||
ariaLabel={t("buttons.exportImage")}
|
aria-label={t("buttons.exportImage")}
|
||||||
>
|
>
|
||||||
{t("buttons.exportImage")}
|
{t("buttons.exportImage")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@ -106,11 +106,11 @@ export const Help = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
dataTestId="help-menu-item"
|
data-testid="help-menu-item"
|
||||||
icon={HelpIcon}
|
icon={HelpIcon}
|
||||||
onSelect={() => actionManager.executeAction(actionShortcuts)}
|
onSelect={() => actionManager.executeAction(actionShortcuts)}
|
||||||
shortcut="?"
|
shortcut="?"
|
||||||
ariaLabel={t("helpDialog.title")}
|
aria-label={t("helpDialog.title")}
|
||||||
>
|
>
|
||||||
{t("helpDialog.title")}
|
{t("helpDialog.title")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@ -136,8 +136,8 @@ export const ClearCanvas = () => {
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
icon={TrashIcon}
|
icon={TrashIcon}
|
||||||
onSelect={toggleDialog}
|
onSelect={toggleDialog}
|
||||||
dataTestId="clear-canvas-button"
|
data-testid="clear-canvas-button"
|
||||||
ariaLabel={t("buttons.clearReset")}
|
aria-label={t("buttons.clearReset")}
|
||||||
>
|
>
|
||||||
{t("buttons.clearReset")}
|
{t("buttons.clearReset")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@ -175,9 +175,9 @@ export const ToggleTheme = () => {
|
|||||||
return actionManager.executeAction(actionToggleTheme);
|
return actionManager.executeAction(actionToggleTheme);
|
||||||
}}
|
}}
|
||||||
icon={appState.theme === "dark" ? SunIcon : MoonIcon}
|
icon={appState.theme === "dark" ? SunIcon : MoonIcon}
|
||||||
dataTestId="toggle-dark-mode"
|
data-testid="toggle-dark-mode"
|
||||||
shortcut={getShortcutFromShortcutName("toggleTheme")}
|
shortcut={getShortcutFromShortcutName("toggleTheme")}
|
||||||
ariaLabel={
|
aria-label={
|
||||||
appState.theme === "dark"
|
appState.theme === "dark"
|
||||||
? t("buttons.lightMode")
|
? t("buttons.lightMode")
|
||||||
: t("buttons.darkMode")
|
: t("buttons.darkMode")
|
||||||
@ -222,8 +222,8 @@ export const Export = () => {
|
|||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
setAppState({ openDialog: "jsonExport" });
|
setAppState({ openDialog: "jsonExport" });
|
||||||
}}
|
}}
|
||||||
dataTestId="json-export-button"
|
data-testid="json-export-button"
|
||||||
ariaLabel={t("buttons.export")}
|
aria-label={t("buttons.export")}
|
||||||
>
|
>
|
||||||
{t("buttons.export")}
|
{t("buttons.export")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@ -236,21 +236,21 @@ export const Socials = () => (
|
|||||||
<DropdownMenuItemLink
|
<DropdownMenuItemLink
|
||||||
icon={GithubIcon}
|
icon={GithubIcon}
|
||||||
href="https://github.com/excalidraw/excalidraw"
|
href="https://github.com/excalidraw/excalidraw"
|
||||||
ariaLabel="GitHub"
|
aria-label="GitHub"
|
||||||
>
|
>
|
||||||
GitHub
|
GitHub
|
||||||
</DropdownMenuItemLink>
|
</DropdownMenuItemLink>
|
||||||
<DropdownMenuItemLink
|
<DropdownMenuItemLink
|
||||||
icon={DiscordIcon}
|
icon={DiscordIcon}
|
||||||
href="https://discord.gg/UexuTaE"
|
href="https://discord.gg/UexuTaE"
|
||||||
ariaLabel="Discord"
|
aria-label="Discord"
|
||||||
>
|
>
|
||||||
Discord
|
Discord
|
||||||
</DropdownMenuItemLink>
|
</DropdownMenuItemLink>
|
||||||
<DropdownMenuItemLink
|
<DropdownMenuItemLink
|
||||||
icon={TwitterIcon}
|
icon={TwitterIcon}
|
||||||
href="https://twitter.com/excalidraw"
|
href="https://twitter.com/excalidraw"
|
||||||
ariaLabel="Twitter"
|
aria-label="Twitter"
|
||||||
>
|
>
|
||||||
Twitter
|
Twitter
|
||||||
</DropdownMenuItemLink>
|
</DropdownMenuItemLink>
|
||||||
@ -258,7 +258,7 @@ export const Socials = () => (
|
|||||||
);
|
);
|
||||||
Socials.displayName = "Socials";
|
Socials.displayName = "Socials";
|
||||||
|
|
||||||
export const LiveCollaboration = ({
|
export const LiveCollaborationTrigger = ({
|
||||||
onSelect,
|
onSelect,
|
||||||
isCollaborating,
|
isCollaborating,
|
||||||
}: {
|
}: {
|
||||||
@ -270,8 +270,8 @@ export const LiveCollaboration = ({
|
|||||||
const appState = useExcalidrawAppState();
|
const appState = useExcalidrawAppState();
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
dataTestId="collab-button"
|
data-testid="collab-button"
|
||||||
icon={UsersIcon}
|
icon={usersIcon}
|
||||||
className={clsx({
|
className={clsx({
|
||||||
"active-collab": isCollaborating,
|
"active-collab": isCollaborating,
|
||||||
})}
|
})}
|
||||||
@ -282,4 +282,4 @@ export const LiveCollaboration = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
LiveCollaboration.displayName = "LiveCollaboration";
|
LiveCollaborationTrigger.displayName = "LiveCollaborationTrigger";
|
195
src/components/welcome-screen/WelcomeScreen.Center.tsx
Normal file
195
src/components/welcome-screen/WelcomeScreen.Center.tsx
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
import { actionLoadScene, actionShortcuts } from "../../actions";
|
||||||
|
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
||||||
|
import { t } from "../../i18n";
|
||||||
|
import {
|
||||||
|
useDevice,
|
||||||
|
useExcalidrawActionManager,
|
||||||
|
useExcalidrawAppState,
|
||||||
|
} from "../App";
|
||||||
|
import { ExcalLogo, HelpIcon, LoadIcon, usersIcon } from "../icons";
|
||||||
|
|
||||||
|
const WelcomeScreenMenuItemContent = ({
|
||||||
|
icon,
|
||||||
|
shortcut,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
icon?: JSX.Element;
|
||||||
|
shortcut?: string | null;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
const device = useDevice();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="welcome-screen-menu-item__icon">{icon}</div>
|
||||||
|
<div className="welcome-screen-menu-item__text">{children}</div>
|
||||||
|
{shortcut && !device.isMobile && (
|
||||||
|
<div className="welcome-screen-menu-item__shortcut">{shortcut}</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
WelcomeScreenMenuItemContent.displayName = "WelcomeScreenMenuItemContent";
|
||||||
|
|
||||||
|
const WelcomeScreenMenuItem = ({
|
||||||
|
onSelect,
|
||||||
|
children,
|
||||||
|
icon,
|
||||||
|
shortcut,
|
||||||
|
className = "",
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
onSelect: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
icon?: JSX.Element;
|
||||||
|
shortcut?: string | null;
|
||||||
|
} & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
{...props}
|
||||||
|
type="button"
|
||||||
|
className={`welcome-screen-menu-item ${className}`}
|
||||||
|
onClick={onSelect}
|
||||||
|
>
|
||||||
|
<WelcomeScreenMenuItemContent icon={icon} shortcut={shortcut}>
|
||||||
|
{children}
|
||||||
|
</WelcomeScreenMenuItemContent>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
WelcomeScreenMenuItem.displayName = "WelcomeScreenMenuItem";
|
||||||
|
|
||||||
|
const WelcomeScreenMenuItemLink = ({
|
||||||
|
children,
|
||||||
|
href,
|
||||||
|
icon,
|
||||||
|
shortcut,
|
||||||
|
className = "",
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
href: string;
|
||||||
|
icon?: JSX.Element;
|
||||||
|
shortcut?: string | null;
|
||||||
|
} & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
{...props}
|
||||||
|
className={`welcome-screen-menu-item ${className}`}
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<WelcomeScreenMenuItemContent icon={icon} shortcut={shortcut}>
|
||||||
|
{children}
|
||||||
|
</WelcomeScreenMenuItemContent>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
WelcomeScreenMenuItemLink.displayName = "WelcomeScreenMenuItemLink";
|
||||||
|
|
||||||
|
const Center = ({ children }: { children?: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<div className="welcome-screen-center">
|
||||||
|
{children || (
|
||||||
|
<>
|
||||||
|
<Logo />
|
||||||
|
<Heading>{t("welcomeScreen.defaults.center_heading")}</Heading>
|
||||||
|
<Menu>
|
||||||
|
<MenuItemLoadScene />
|
||||||
|
<MenuItemHelp />
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
Center.displayName = "Center";
|
||||||
|
|
||||||
|
const Logo = ({ children }: { children?: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<div className="welcome-screen-center__logo virgil welcome-screen-decor">
|
||||||
|
{children || <>{ExcalLogo} Excalidraw</>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
Logo.displayName = "Logo";
|
||||||
|
|
||||||
|
const Heading = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<div className="welcome-screen-center__heading welcome-screen-decor virgil">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
Heading.displayName = "Heading";
|
||||||
|
|
||||||
|
const Menu = ({ children }: { children?: React.ReactNode }) => {
|
||||||
|
return <div className="welcome-screen-menu">{children}</div>;
|
||||||
|
};
|
||||||
|
Menu.displayName = "Menu";
|
||||||
|
|
||||||
|
const MenuItemHelp = () => {
|
||||||
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WelcomeScreenMenuItem
|
||||||
|
onSelect={() => actionManager.executeAction(actionShortcuts)}
|
||||||
|
shortcut="?"
|
||||||
|
icon={HelpIcon}
|
||||||
|
>
|
||||||
|
{t("helpDialog.title")}
|
||||||
|
</WelcomeScreenMenuItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
MenuItemHelp.displayName = "MenuItemHelp";
|
||||||
|
|
||||||
|
const MenuItemLoadScene = () => {
|
||||||
|
const appState = useExcalidrawAppState();
|
||||||
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
|
if (appState.viewModeEnabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WelcomeScreenMenuItem
|
||||||
|
onSelect={() => actionManager.executeAction(actionLoadScene)}
|
||||||
|
shortcut={getShortcutFromShortcutName("loadScene")}
|
||||||
|
icon={LoadIcon}
|
||||||
|
>
|
||||||
|
{t("buttons.load")}
|
||||||
|
</WelcomeScreenMenuItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
MenuItemLoadScene.displayName = "MenuItemLoadScene";
|
||||||
|
|
||||||
|
const MenuItemLiveCollaborationTrigger = ({
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
onSelect: () => any;
|
||||||
|
}) => {
|
||||||
|
// FIXME when we tie t() to lang state
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const appState = useExcalidrawAppState();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WelcomeScreenMenuItem shortcut={null} onSelect={onSelect} icon={usersIcon}>
|
||||||
|
{t("labels.liveCollaboration")}
|
||||||
|
</WelcomeScreenMenuItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
MenuItemLiveCollaborationTrigger.displayName =
|
||||||
|
"MenuItemLiveCollaborationTrigger";
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Center.Logo = Logo;
|
||||||
|
Center.Heading = Heading;
|
||||||
|
Center.Menu = Menu;
|
||||||
|
Center.MenuItem = WelcomeScreenMenuItem;
|
||||||
|
Center.MenuItemLink = WelcomeScreenMenuItemLink;
|
||||||
|
Center.MenuItemHelp = MenuItemHelp;
|
||||||
|
Center.MenuItemLoadScene = MenuItemLoadScene;
|
||||||
|
Center.MenuItemLiveCollaborationTrigger = MenuItemLiveCollaborationTrigger;
|
||||||
|
|
||||||
|
export { Center };
|
42
src/components/welcome-screen/WelcomeScreen.Hints.tsx
Normal file
42
src/components/welcome-screen/WelcomeScreen.Hints.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { t } from "../../i18n";
|
||||||
|
import {
|
||||||
|
WelcomeScreenHelpArrow,
|
||||||
|
WelcomeScreenMenuArrow,
|
||||||
|
WelcomeScreenTopToolbarArrow,
|
||||||
|
} from "../icons";
|
||||||
|
|
||||||
|
const MenuHint = ({ children }: { children?: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--menu">
|
||||||
|
{WelcomeScreenMenuArrow}
|
||||||
|
<div className="welcome-screen-decor-hint__label">
|
||||||
|
{children || t("welcomeScreen.defaults.menuHint")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
MenuHint.displayName = "MenuHint";
|
||||||
|
|
||||||
|
const ToolbarHint = ({ children }: { children?: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--toolbar">
|
||||||
|
<div className="welcome-screen-decor-hint__label">
|
||||||
|
{children || t("welcomeScreen.defaults.toolbarHint")}
|
||||||
|
</div>
|
||||||
|
{WelcomeScreenTopToolbarArrow}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
ToolbarHint.displayName = "ToolbarHint";
|
||||||
|
|
||||||
|
const HelpHint = ({ children }: { children?: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--help">
|
||||||
|
<div>{children || t("welcomeScreen.defaults.helpHint")}</div>
|
||||||
|
{WelcomeScreenHelpArrow}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
HelpHint.displayName = "HelpHint";
|
||||||
|
|
||||||
|
export { HelpHint, MenuHint, ToolbarHint };
|
@ -3,29 +3,39 @@
|
|||||||
font-family: "Virgil";
|
font-family: "Virgil";
|
||||||
}
|
}
|
||||||
|
|
||||||
.WelcomeScreen-logo {
|
// WelcomeSreen common
|
||||||
display: flex;
|
// ---------------------------------------------------------------------------
|
||||||
align-items: center;
|
|
||||||
column-gap: 0.75rem;
|
|
||||||
font-size: 2.25rem;
|
|
||||||
|
|
||||||
svg {
|
.welcome-screen-decor {
|
||||||
width: 1.625rem;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.WelcomeScreen-decor {
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
color: var(--color-gray-40);
|
color: var(--color-gray-40);
|
||||||
|
|
||||||
&--subheading {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&--help-pointer {
|
&.theme--dark {
|
||||||
|
.welcome-screen-decor {
|
||||||
|
color: var(--color-gray-60);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WelcomeScreen.Hints
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
.welcome-screen-decor-hint {
|
||||||
|
@media (max-height: 599px) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px), (max-width: 800px) {
|
||||||
|
.welcome-screen-decor {
|
||||||
|
&--help,
|
||||||
|
&--menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--help {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
@ -49,7 +59,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--top-toolbar-pointer {
|
&--toolbar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -58,7 +68,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
|
|
||||||
&__label {
|
.welcome-screen-decor-hint__label {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -0.5rem;
|
top: -0.5rem;
|
||||||
@ -74,7 +84,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--menu-pointer {
|
&--menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 320px;
|
width: 320px;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
@ -95,10 +105,19 @@
|
|||||||
transform: scaleX(-1);
|
transform: scaleX(-1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.welcome-screen-decor-hint__label {
|
||||||
|
max-width: 160px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.WelcomeScreen-container {
|
// WelcomeSreen.Center
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
.welcome-screen-center {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
@ -112,7 +131,24 @@
|
|||||||
bottom: 1rem;
|
bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.WelcomeScreen-items {
|
.welcome-screen-center__logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 0.75rem;
|
||||||
|
font-size: 2.25rem;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 1.625rem;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-screen-center__heading {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-screen-menu {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
@ -120,7 +156,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.WelcomeScreen-item {
|
.welcome-screen-menu-item {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
@ -128,8 +164,10 @@
|
|||||||
color: var(--color-gray-50);
|
color: var(--color-gray-50);
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
display: flex;
|
max-width: 400px;
|
||||||
|
display: grid;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
@ -140,44 +178,49 @@
|
|||||||
|
|
||||||
border-radius: var(--border-radius-md);
|
border-radius: var(--border-radius-md);
|
||||||
|
|
||||||
&__label {
|
grid-template-columns: calc(var(--default-icon-size) + 0.5rem) 1fr 3rem;
|
||||||
|
|
||||||
|
&__text {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
margin-right: auto;
|
||||||
|
text-align: left;
|
||||||
column-gap: 0.5rem;
|
column-gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
svg {
|
&__icon {
|
||||||
width: var(--default-icon-size);
|
width: var(--default-icon-size);
|
||||||
height: var(--default-icon-size);
|
height: var(--default-icon-size);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&__shortcut {
|
&__shortcut {
|
||||||
|
margin-left: auto;
|
||||||
color: var(--color-gray-40);
|
color: var(--color-gray-40);
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:active) .WelcomeScreen-item:hover {
|
&:not(:active) .welcome-screen-menu-item:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background: var(--color-gray-10);
|
background: var(--color-gray-10);
|
||||||
|
|
||||||
.WelcomeScreen-item__shortcut {
|
.welcome-screen-menu-item__shortcut {
|
||||||
color: var(--color-gray-50);
|
color: var(--color-gray-50);
|
||||||
}
|
}
|
||||||
|
|
||||||
.WelcomeScreen-item__label {
|
.welcome-screen-menu-item__text {
|
||||||
color: var(--color-gray-100);
|
color: var(--color-gray-100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.WelcomeScreen-item:active {
|
.welcome-screen-menu-item:active {
|
||||||
background: var(--color-gray-20);
|
background: var(--color-gray-20);
|
||||||
|
|
||||||
.WelcomeScreen-item__shortcut {
|
.welcome-screen-menu-item__shortcut {
|
||||||
color: var(--color-gray-50);
|
color: var(--color-gray-50);
|
||||||
}
|
}
|
||||||
|
|
||||||
.WelcomeScreen-item__label {
|
.welcome-screen-menu-item__text {
|
||||||
color: var(--color-gray-100);
|
color: var(--color-gray-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,7 +228,7 @@
|
|||||||
color: var(--color-promo) !important;
|
color: var(--color-promo) !important;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
.WelcomeScreen-item__label {
|
.welcome-screen-menu-item__text {
|
||||||
color: var(--color-promo) !important;
|
color: var(--color-promo) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -193,11 +236,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.theme--dark {
|
&.theme--dark {
|
||||||
.WelcomeScreen-decor {
|
.welcome-screen-menu-item {
|
||||||
color: var(--color-gray-60);
|
|
||||||
}
|
|
||||||
|
|
||||||
.WelcomeScreen-item {
|
|
||||||
color: var(--color-gray-60);
|
color: var(--color-gray-60);
|
||||||
|
|
||||||
&__shortcut {
|
&__shortcut {
|
||||||
@ -205,69 +244,41 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:active) .WelcomeScreen-item:hover {
|
&:not(:active) .welcome-screen-menu-item:hover {
|
||||||
background: var(--color-gray-85);
|
background: var(--color-gray-85);
|
||||||
|
|
||||||
.WelcomeScreen-item__shortcut {
|
.welcome-screen-menu-item__shortcut {
|
||||||
color: var(--color-gray-50);
|
color: var(--color-gray-50);
|
||||||
}
|
}
|
||||||
|
|
||||||
.WelcomeScreen-item__label {
|
.welcome-screen-menu-item__text {
|
||||||
color: var(--color-gray-10);
|
color: var(--color-gray-10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.WelcomeScreen-item:active {
|
.welcome-screen-menu-item:active {
|
||||||
background-color: var(--color-gray-90);
|
background-color: var(--color-gray-90);
|
||||||
.WelcomeScreen-item__label {
|
.welcome-screen-menu-item__text {
|
||||||
color: var(--color-gray-10);
|
color: var(--color-gray-10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Can tweak these values but for an initial effort, it looks OK to me
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.WelcomeScreen-decor {
|
|
||||||
&--help-pointer,
|
|
||||||
&--menu-pointer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// @media (max-height: 400px) {
|
|
||||||
// .WelcomeScreen-container {
|
|
||||||
// margin-top: 0;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@media (max-height: 599px) {
|
@media (max-height: 599px) {
|
||||||
.WelcomeScreen-container {
|
.welcome-screen-center {
|
||||||
margin-top: 4rem;
|
margin-top: 4rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-height: 600px) and (max-height: 900px) {
|
@media (min-height: 600px) and (max-height: 900px) {
|
||||||
.WelcomeScreen-container {
|
.welcome-screen-center {
|
||||||
margin-top: 8rem;
|
margin-top: 8rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (max-height: 630px) {
|
@media (max-height: 500px), (max-width: 320px) {
|
||||||
.WelcomeScreen-decor--top-toolbar-pointer {
|
.welcome-screen-center {
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-height: 500px) {
|
|
||||||
.WelcomeScreen-container {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @media (max-height: 740px) {
|
// ---------------------------------------------------------------------------
|
||||||
// .WelcomeScreen-decor {
|
|
||||||
// &--help-pointer,
|
|
||||||
// &--top-toolbar-pointer,
|
|
||||||
// &--menu-pointer {
|
|
||||||
// display: none;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
17
src/components/welcome-screen/WelcomeScreen.tsx
Normal file
17
src/components/welcome-screen/WelcomeScreen.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Center } from "./WelcomeScreen.Center";
|
||||||
|
import { MenuHint, ToolbarHint, HelpHint } from "./WelcomeScreen.Hints";
|
||||||
|
|
||||||
|
import "./WelcomeScreen.scss";
|
||||||
|
|
||||||
|
const WelcomeScreen = (props: { children: React.ReactNode }) => {
|
||||||
|
// NOTE this component is used as a dummy wrapper to retrieve child props
|
||||||
|
// from, and will never be rendered to DOM directly. As such, we can't
|
||||||
|
// do anything here (use hooks and such)
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
WelcomeScreen.displayName = "WelcomeScreen";
|
||||||
|
|
||||||
|
WelcomeScreen.Center = Center;
|
||||||
|
WelcomeScreen.Hints = { MenuHint, ToolbarHint, HelpHint };
|
||||||
|
|
||||||
|
export default WelcomeScreen;
|
@ -150,6 +150,7 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
|
|||||||
toggleTheme: null,
|
toggleTheme: null,
|
||||||
saveAsImage: true,
|
saveAsImage: true,
|
||||||
},
|
},
|
||||||
|
welcomeScreen: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// breakpoints
|
// breakpoints
|
||||||
@ -236,14 +237,6 @@ export const ROUNDNESS = {
|
|||||||
ADAPTIVE_RADIUS: 3,
|
ADAPTIVE_RADIUS: 3,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const COOKIES = {
|
|
||||||
AUTH_STATE_COOKIE: "excplus-auth",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/** key containt id of precedeing elemnt id we use in reconciliation during
|
/** key containt id of precedeing elemnt id we use in reconciliation during
|
||||||
* collaboration */
|
* collaboration */
|
||||||
export const PRECEDING_ELEMENT_KEY = "__precedingElement__";
|
export const PRECEDING_ELEMENT_KEY = "__precedingElement__";
|
||||||
|
|
||||||
export const isExcalidrawPlusSignedUser = document.cookie.includes(
|
|
||||||
COOKIES.AUTH_STATE_COOKIE,
|
|
||||||
);
|
|
||||||
|
@ -408,7 +408,7 @@
|
|||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--button-hover);
|
background-color: var(--button-hover-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
@ -540,9 +540,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mobile-misc-tools-container {
|
.mobile-misc-tools-container {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
top: 5rem;
|
top: calc(5rem - var(--editor-container-padding));
|
||||||
right: 0;
|
right: calc(var(--editor-container-padding) * -1);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border: 1px solid var(--sidebar-border-color);
|
border: 1px solid var(--sidebar-border-color);
|
||||||
|
@ -35,13 +35,14 @@
|
|||||||
--shadow-island: 0px 7px 14px rgba(0, 0, 0, 0.05),
|
--shadow-island: 0px 7px 14px rgba(0, 0, 0, 0.05),
|
||||||
0px 0px 3.12708px rgba(0, 0, 0, 0.0798),
|
0px 0px 3.12708px rgba(0, 0, 0, 0.0798),
|
||||||
0px 0px 0.931014px rgba(0, 0, 0, 0.1702);
|
0px 0px 0.931014px rgba(0, 0, 0, 0.1702);
|
||||||
--button-hover: var(--color-gray-10);
|
--button-hover-bg: var(--color-gray-10);
|
||||||
--default-border-color: var(--color-gray-30);
|
--default-border-color: var(--color-gray-30);
|
||||||
|
|
||||||
--default-button-size: 2rem;
|
--default-button-size: 2rem;
|
||||||
--default-icon-size: 1rem;
|
--default-icon-size: 1rem;
|
||||||
--lg-button-size: 2.25rem;
|
--lg-button-size: 2.25rem;
|
||||||
--lg-icon-size: 1rem;
|
--lg-icon-size: 1rem;
|
||||||
|
--editor-container-padding: 1rem;
|
||||||
|
|
||||||
@media screen and (min-device-width: 1921px) {
|
@media screen and (min-device-width: 1921px) {
|
||||||
--lg-button-size: 2.5rem;
|
--lg-button-size: 2.5rem;
|
||||||
@ -135,7 +136,7 @@
|
|||||||
--popup-text-inverted-color: #2c2c2c;
|
--popup-text-inverted-color: #2c2c2c;
|
||||||
--select-highlight-color: #{$oc-blue-4};
|
--select-highlight-color: #{$oc-blue-4};
|
||||||
--text-primary-color: var(--color-gray-40);
|
--text-primary-color: var(--color-gray-40);
|
||||||
--button-hover: var(--color-gray-80);
|
--button-hover-bg: var(--color-gray-80);
|
||||||
--default-border-color: var(--color-gray-80);
|
--default-border-color: var(--color-gray-80);
|
||||||
--shadow-island: 0px 13px 33px rgba(0, 0, 0, 0.07),
|
--shadow-island: 0px 13px 33px rgba(0, 0, 0, 0.07),
|
||||||
0px 4.13px 9.94853px rgba(0, 0, 0, 0.0456112),
|
0px 4.13px 9.94853px rgba(0, 0, 0, 0.0456112),
|
||||||
|
@ -39,11 +39,11 @@
|
|||||||
|
|
||||||
.ToolIcon__icon {
|
.ToolIcon__icon {
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--button-hover);
|
background: var(--button-hover-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background: var(--button-hover);
|
background: var(--button-hover-bg);
|
||||||
border: 1px solid var(--color-primary-darkest);
|
border: 1px solid var(--color-primary-darkest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -54,24 +54,25 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.625rem;
|
padding: 0.625rem;
|
||||||
width: var(--default-button-size);
|
width: var(--button-width, var(--default-button-size));
|
||||||
height: var(--default-button-size);
|
height: var(--button-height, var(--default-button-size));
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-color: var(--default-border-color);
|
border-color: var(--button-border, var(--default-border-color));
|
||||||
border-radius: var(--border-radius-lg);
|
border-radius: var(--border-radius-lg);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: transparent;
|
background-color: var(--button-bg, var(--island-bg-color));
|
||||||
color: var(--text-primary-color);
|
color: var(--button-color, var(--text-primary-color));
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--button-hover);
|
background-color: var(--button-hover-bg);
|
||||||
|
border-color: var(--button-hover-border, var(--default-border-color));
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background-color: var(--button-hover);
|
background-color: var(--button-active-bg);
|
||||||
border-color: var(--color-primary-darkest);
|
border-color: var(--button-active-border, var(--color-primary-darkest));
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
@ -83,7 +84,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
color: var(--color-primary-darker);
|
color: var(--button-color, var(--color-primary-darker));
|
||||||
|
|
||||||
|
width: var(--button-width, var(--lg-icon-size));
|
||||||
|
height: var(--button-height, var(--lg-icon-size));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -557,10 +557,10 @@ export const resizeSingleElement = (
|
|||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
scale: [
|
scale: [
|
||||||
// defaulting because scaleX/Y can be 0/-0
|
// defaulting because scaleX/Y can be 0/-0
|
||||||
(Math.sign(scaleX) || stateAtResizeStart.scale[0]) *
|
(Math.sign(newBoundsX2 - stateAtResizeStart.x) ||
|
||||||
stateAtResizeStart.scale[0],
|
stateAtResizeStart.scale[0]) * stateAtResizeStart.scale[0],
|
||||||
(Math.sign(scaleY) || stateAtResizeStart.scale[1]) *
|
(Math.sign(newBoundsY2 - stateAtResizeStart.y) ||
|
||||||
stateAtResizeStart.scale[1],
|
stateAtResizeStart.scale[1]) * stateAtResizeStart.scale[1],
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -38,3 +38,11 @@ export const STORAGE_KEYS = {
|
|||||||
VERSION_DATA_STATE: "version-dataState",
|
VERSION_DATA_STATE: "version-dataState",
|
||||||
VERSION_FILES: "version-files",
|
VERSION_FILES: "version-files",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const COOKIES = {
|
||||||
|
AUTH_STATE_COOKIE: "excplus-auth",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const isExcalidrawPlusSignedUser = document.cookie.includes(
|
||||||
|
COOKIES.AUTH_STATE_COOKIE,
|
||||||
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { isExcalidrawPlusSignedUser } from "../../constants";
|
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||||
|
|
||||||
export const ExcalidrawPlusAppLink = () => {
|
export const ExcalidrawPlusAppLink = () => {
|
||||||
if (!isExcalidrawPlusSignedUser) {
|
if (!isExcalidrawPlusSignedUser) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import polyfill from "../polyfill";
|
import polyfill from "../polyfill";
|
||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
import { ErrorDialog } from "../components/ErrorDialog";
|
import { ErrorDialog } from "../components/ErrorDialog";
|
||||||
@ -26,6 +26,8 @@ import {
|
|||||||
defaultLang,
|
defaultLang,
|
||||||
Footer,
|
Footer,
|
||||||
MainMenu,
|
MainMenu,
|
||||||
|
LiveCollaborationTrigger,
|
||||||
|
WelcomeScreen,
|
||||||
} from "../packages/excalidraw/index";
|
} from "../packages/excalidraw/index";
|
||||||
import {
|
import {
|
||||||
AppState,
|
AppState,
|
||||||
@ -45,6 +47,7 @@ import {
|
|||||||
} from "../utils";
|
} from "../utils";
|
||||||
import {
|
import {
|
||||||
FIREBASE_STORAGE_PREFIXES,
|
FIREBASE_STORAGE_PREFIXES,
|
||||||
|
isExcalidrawPlusSignedUser,
|
||||||
STORAGE_KEYS,
|
STORAGE_KEYS,
|
||||||
SYNC_BROWSER_TABS_TIMEOUT,
|
SYNC_BROWSER_TABS_TIMEOUT,
|
||||||
} from "./app_constants";
|
} from "./app_constants";
|
||||||
@ -608,7 +611,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
<MainMenu.DefaultItems.SaveToActiveFile />
|
<MainMenu.DefaultItems.SaveToActiveFile />
|
||||||
<MainMenu.DefaultItems.Export />
|
<MainMenu.DefaultItems.Export />
|
||||||
<MainMenu.DefaultItems.SaveAsImage />
|
<MainMenu.DefaultItems.SaveAsImage />
|
||||||
<MainMenu.DefaultItems.LiveCollaboration
|
<MainMenu.DefaultItems.LiveCollaborationTrigger
|
||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
onSelect={() => setCollabDialogShown(true)}
|
onSelect={() => setCollabDialogShown(true)}
|
||||||
/>
|
/>
|
||||||
@ -634,6 +637,63 @@ const ExcalidrawWrapper = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const welcomeScreenJSX = useMemo(() => {
|
||||||
|
let headingContent;
|
||||||
|
|
||||||
|
if (isExcalidrawPlusSignedUser) {
|
||||||
|
headingContent = t("welcomeScreen.app.center_heading_plus")
|
||||||
|
.split(/(Excalidraw\+)/)
|
||||||
|
.map((bit, idx) => {
|
||||||
|
if (bit === "Excalidraw+") {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
style={{ pointerEvents: "all" }}
|
||||||
|
href={`${process.env.REACT_APP_PLUS_APP}?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`}
|
||||||
|
key={idx}
|
||||||
|
>
|
||||||
|
Excalidraw+
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return bit;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
headingContent = t("welcomeScreen.app.center_heading");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WelcomeScreen>
|
||||||
|
<WelcomeScreen.Hints.MenuHint>
|
||||||
|
{t("welcomeScreen.app.menuHint")}
|
||||||
|
</WelcomeScreen.Hints.MenuHint>
|
||||||
|
<WelcomeScreen.Hints.ToolbarHint />
|
||||||
|
<WelcomeScreen.Hints.HelpHint />
|
||||||
|
<WelcomeScreen.Center>
|
||||||
|
<WelcomeScreen.Center.Logo />
|
||||||
|
<WelcomeScreen.Center.Heading>
|
||||||
|
{headingContent}
|
||||||
|
</WelcomeScreen.Center.Heading>
|
||||||
|
<WelcomeScreen.Center.Menu>
|
||||||
|
<WelcomeScreen.Center.MenuItemLoadScene />
|
||||||
|
<WelcomeScreen.Center.MenuItemHelp />
|
||||||
|
<WelcomeScreen.Center.MenuItemLiveCollaborationTrigger
|
||||||
|
onSelect={() => setCollabDialogShown(true)}
|
||||||
|
/>
|
||||||
|
{!isExcalidrawPlusSignedUser && (
|
||||||
|
<WelcomeScreen.Center.MenuItemLink
|
||||||
|
href="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
|
||||||
|
shortcut={null}
|
||||||
|
icon={PlusPromoIcon}
|
||||||
|
>
|
||||||
|
Try Excalidraw Plus!
|
||||||
|
</WelcomeScreen.Center.MenuItemLink>
|
||||||
|
)}
|
||||||
|
</WelcomeScreen.Center.Menu>
|
||||||
|
</WelcomeScreen.Center>
|
||||||
|
</WelcomeScreen>
|
||||||
|
);
|
||||||
|
}, [setCollabDialogShown]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{ height: "100%" }}
|
style={{ height: "100%" }}
|
||||||
@ -645,7 +705,6 @@ const ExcalidrawWrapper = () => {
|
|||||||
ref={excalidrawRefCallback}
|
ref={excalidrawRefCallback}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
initialData={initialStatePromiseRef.current.promise}
|
initialData={initialStatePromiseRef.current.promise}
|
||||||
onCollabButtonClick={() => setCollabDialogShown(true)}
|
|
||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
onPointerUpdate={collabAPI?.onPointerUpdate}
|
onPointerUpdate={collabAPI?.onPointerUpdate}
|
||||||
UIOptions={{
|
UIOptions={{
|
||||||
@ -679,14 +738,27 @@ const ExcalidrawWrapper = () => {
|
|||||||
onLibraryChange={onLibraryChange}
|
onLibraryChange={onLibraryChange}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
renderTopRightUI={(isMobile) => {
|
||||||
|
if (isMobile) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<LiveCollaborationTrigger
|
||||||
|
isCollaborating={isCollaborating}
|
||||||
|
onSelect={() => setCollabDialogShown(true)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{renderMenu()}
|
{renderMenu()}
|
||||||
|
|
||||||
<Footer>
|
<Footer>
|
||||||
<div style={{ display: "flex", gap: ".5rem", alignItems: "center" }}>
|
<div style={{ display: "flex", gap: ".5rem", alignItems: "center" }}>
|
||||||
<ExcalidrawPlusAppLink />
|
<ExcalidrawPlusAppLink />
|
||||||
<EncryptedIcon />
|
<EncryptedIcon />
|
||||||
</div>
|
</div>
|
||||||
</Footer>
|
</Footer>
|
||||||
|
{welcomeScreenJSX}
|
||||||
</Excalidraw>
|
</Excalidraw>
|
||||||
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
|
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
|
@ -448,10 +448,16 @@
|
|||||||
"d9480f": "Orange 9"
|
"d9480f": "Orange 9"
|
||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"data": "All your data is saved locally in your browser.",
|
"app": {
|
||||||
"switchToPlusApp": "Did you want to go to the Excalidraw+ instead?",
|
"center_heading": "All your data is saved locally in your browser.",
|
||||||
"menuHints": "Export, preferences, languages, ...",
|
"center_heading_plus": "Did you want to go to the Excalidraw+ instead?",
|
||||||
"toolbarHints": "Pick a tool & Start drawing!",
|
"menuHint": "Export, preferences, languages, ..."
|
||||||
"helpHints": "Shortcuts & help"
|
},
|
||||||
|
"defaults": {
|
||||||
|
"menuHint": "Export, preferences, and more...",
|
||||||
|
"center_heading": "Diagrams. Made. Simple.",
|
||||||
|
"toolbarHint": "Pick a tool & Start drawing!",
|
||||||
|
"helpHint": "Shortcuts & help"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,22 +11,195 @@ The change should be grouped under one of the below section and must contain PR
|
|||||||
Please add the latest change on the top under the correct section.
|
Please add the latest change on the top under the correct section.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## Unreleased
|
## 0.14.1 (2023-01-16)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- remove overflow hidden from button [#6110](https://github.com/excalidraw/excalidraw/pull/6110). This fixes the collaborator count css in the [LiveCollaborationTrigger](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#LiveCollaborationTrigger) component.
|
||||||
|
|
||||||
|
## 0.14.0 (2023-01-13)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
|
- Support customization for the editor [welcome screen](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#WelcomeScreen) [#6048](https://github.com/excalidraw/excalidraw/pull/6048).
|
||||||
|
|
||||||
- Expose component API for the Excalidraw main menu [#6034](https://github.com/excalidraw/excalidraw/pull/6034), You can read more about its usage [here](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#MainMenu)
|
- Expose component API for the Excalidraw main menu [#6034](https://github.com/excalidraw/excalidraw/pull/6034), You can read more about its usage [here](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#MainMenu)
|
||||||
|
|
||||||
- Render Footer as a component instead of render prop [#5970](https://github.com/excalidraw/excalidraw/pull/5970). You can read more about its usage [here](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#Footer)
|
- Support customization for the Excalidraw [main menu](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#MainMenu) [#6034](https://github.com/excalidraw/excalidraw/pull/6034).
|
||||||
|
|
||||||
#### BREAKING CHANGE
|
- [Footer](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#Footer) is now rendered as child component instead of passed as a render prop [#5970](https://github.com/excalidraw/excalidraw/pull/5970).
|
||||||
|
|
||||||
- With this change, the prop `renderFooter` is now removed.
|
- Any top-level children passed to the `<Excalidraw/>` component that do not belong to one of the officially supported Excalidraw children components are now rendered directly inside the Excalidraw container (previously, they weren't rendered at all) [#6096](https://github.com/excalidraw/excalidraw/pull/6096).
|
||||||
|
|
||||||
|
- Expose [LiveCollaborationTrigger](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#LiveCollaborationTrigger) component. Replaces `props.onCollabButtonClick` [#6104](https://github.com/excalidraw/excalidraw/pull/6104).
|
||||||
|
|
||||||
|
#### BREAKING CHANGES
|
||||||
|
|
||||||
|
- `props.onCollabButtonClick` is now removed. You need to render the main menu item yourself, and optionally also render the `<LiveCollaborationTrigger>` component using [renderTopRightUI](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#renderTopRightUI) prop if you want to retain the canvas button at top-right.
|
||||||
|
- The prop `renderFooter` is now removed in favor of rendering as a child component.
|
||||||
|
|
||||||
### Excalidraw schema
|
### Excalidraw schema
|
||||||
|
|
||||||
- Merged `appState.currentItemStrokeSharpness` and `appState.currentItemLinearStrokeSharpness` into `appState.currentItemRoundness`. Renamed `changeSharpness` action to `changeRoundness`. Excalidraw element's `strokeSharpness` was changed to `roundness`. Check the PR for types and more details [#5553](https://github.com/excalidraw/excalidraw/pull/5553).
|
- Merged `appState.currentItemStrokeSharpness` and `appState.currentItemLinearStrokeSharpness` into `appState.currentItemRoundness`. Renamed `changeSharpness` action to `changeRoundness`. Excalidraw element's `strokeSharpness` was changed to `roundness`. Check the PR for types and more details [#5553](https://github.com/excalidraw/excalidraw/pull/5553).
|
||||||
|
|
||||||
|
## Excalidraw Library
|
||||||
|
|
||||||
|
**_This section lists the updates made to the excalidraw library and will not affect the integration._**
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Generic button export [#6092](https://github.com/excalidraw/excalidraw/pull/6092)
|
||||||
|
|
||||||
|
- Scroll using PageUp and PageDown [#6038](https://github.com/excalidraw/excalidraw/pull/6038)
|
||||||
|
|
||||||
|
- Support shrinking text containers to original height when text removed [#6025](https://github.com/excalidraw/excalidraw/pull/6025)
|
||||||
|
|
||||||
|
- Move contextMenu into the component tree and control via appState [#6021](https://github.com/excalidraw/excalidraw/pull/6021)
|
||||||
|
|
||||||
|
- Allow readonly actions to be used in viewMode [#5982](https://github.com/excalidraw/excalidraw/pull/5982)
|
||||||
|
|
||||||
|
- Support labels for arrow 🔥 [#5723](https://github.com/excalidraw/excalidraw/pull/5723)
|
||||||
|
|
||||||
|
- Don't add midpoint until dragged beyond a threshold [#5927](https://github.com/excalidraw/excalidraw/pull/5927)
|
||||||
|
|
||||||
|
- Changed text copy/paste behaviour [#5786](https://github.com/excalidraw/excalidraw/pull/5786)
|
||||||
|
|
||||||
|
- Reintroduce `x` shortcut for `freedraw` [#5840](https://github.com/excalidraw/excalidraw/pull/5840)
|
||||||
|
|
||||||
|
- Tweak toolbar shortcuts & remove library shortcut [#5832](https://github.com/excalidraw/excalidraw/pull/5832)
|
||||||
|
|
||||||
|
- Clean unused images only after 24hrs (local-only) [#5839](https://github.com/excalidraw/excalidraw/pull/5839)
|
||||||
|
|
||||||
|
- Refetch errored/pending images on collab room init load [#5833](https://github.com/excalidraw/excalidraw/pull/5833)
|
||||||
|
|
||||||
|
- Stop deleting whole line when no point select in line editor [#5676](https://github.com/excalidraw/excalidraw/pull/5676)
|
||||||
|
|
||||||
|
- Editor redesign 🔥 [#5780](https://github.com/excalidraw/excalidraw/pull/5780)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Mobile tools positioning [#6107](https://github.com/excalidraw/excalidraw/pull/6107)
|
||||||
|
|
||||||
|
- Renamed folder MainMenu->main-menu and support rest props [#6103](https://github.com/excalidraw/excalidraw/pull/6103)
|
||||||
|
|
||||||
|
- Use position absolute for mobile misc tools [#6099](https://github.com/excalidraw/excalidraw/pull/6099)
|
||||||
|
|
||||||
|
- React.memo resolvers not accounting for all props [#6042](https://github.com/excalidraw/excalidraw/pull/6042)
|
||||||
|
|
||||||
|
- Image horizontal flip fix + improved tests [#5799](https://github.com/excalidraw/excalidraw/pull/5799)
|
||||||
|
|
||||||
|
- Png-exporting does not preserve angles correctly for flipped images [#6085](https://github.com/excalidraw/excalidraw/pull/6085)
|
||||||
|
|
||||||
|
- Stale appState of MainMenu defaultItems rendered from Actions [#6074](https://github.com/excalidraw/excalidraw/pull/6074)
|
||||||
|
|
||||||
|
- HelpDialog [#6072](https://github.com/excalidraw/excalidraw/pull/6072)
|
||||||
|
|
||||||
|
- Show error message on collab save failure [#6063](https://github.com/excalidraw/excalidraw/pull/6063)
|
||||||
|
|
||||||
|
- Remove ga from docker build [#6059](https://github.com/excalidraw/excalidraw/pull/6059)
|
||||||
|
|
||||||
|
- Use displayName since name gets stripped off when uglifying/minifiyng in production [#6036](https://github.com/excalidraw/excalidraw/pull/6036)
|
||||||
|
|
||||||
|
- Remove background from wysiwyg when editing arrow label [#6033](https://github.com/excalidraw/excalidraw/pull/6033)
|
||||||
|
|
||||||
|
- Use canvas measureText to calculate width in measureText [#6030](https://github.com/excalidraw/excalidraw/pull/6030)
|
||||||
|
|
||||||
|
- Restoring deleted bindings [#6029](https://github.com/excalidraw/excalidraw/pull/6029)
|
||||||
|
|
||||||
|
- ColorPicker getColor [#5949](https://github.com/excalidraw/excalidraw/pull/5949)
|
||||||
|
|
||||||
|
- Don't push whitespace to next line when exceeding max width during wrapping and make sure to use same width of text editor on DOM when measuring dimensions [#5996](https://github.com/excalidraw/excalidraw/pull/5996)
|
||||||
|
|
||||||
|
- Showing `grabbing` cursor when holding `spacebar` [#6015](https://github.com/excalidraw/excalidraw/pull/6015)
|
||||||
|
|
||||||
|
- Resize sometimes throwing on missing null-checks [#6013](https://github.com/excalidraw/excalidraw/pull/6013)
|
||||||
|
|
||||||
|
- PWA not working after CRA@5 update [#6012](https://github.com/excalidraw/excalidraw/pull/6012)
|
||||||
|
|
||||||
|
- Not properly restoring element stroke and bg colors [#6002](https://github.com/excalidraw/excalidraw/pull/6002)
|
||||||
|
|
||||||
|
- Avatar outline on safari & center [#5997](https://github.com/excalidraw/excalidraw/pull/5997)
|
||||||
|
|
||||||
|
- Chart pasting not working due to removing tab characters [#5987](https://github.com/excalidraw/excalidraw/pull/5987)
|
||||||
|
|
||||||
|
- Apply the right type of roundness when pasting styles [#5979](https://github.com/excalidraw/excalidraw/pull/5979)
|
||||||
|
|
||||||
|
- Remove editor onpaste handler [#5971](https://github.com/excalidraw/excalidraw/pull/5971)
|
||||||
|
|
||||||
|
- Remove blank space [#5950](https://github.com/excalidraw/excalidraw/pull/5950)
|
||||||
|
|
||||||
|
- Galego and Kurdî missing in languages plus two locale typos [#5954](https://github.com/excalidraw/excalidraw/pull/5954)
|
||||||
|
|
||||||
|
- `ExcalidrawArrowElement` rather than `ExcalidrawArrowEleement` [#5955](https://github.com/excalidraw/excalidraw/pull/5955)
|
||||||
|
|
||||||
|
- RenderFooter styling [#5962](https://github.com/excalidraw/excalidraw/pull/5962)
|
||||||
|
|
||||||
|
- Repair element bindings on restore [#5956](https://github.com/excalidraw/excalidraw/pull/5956)
|
||||||
|
|
||||||
|
- Don't allow whitespaces for bound text [#5939](https://github.com/excalidraw/excalidraw/pull/5939)
|
||||||
|
|
||||||
|
- Bindings do not survive history serialization [#5942](https://github.com/excalidraw/excalidraw/pull/5942)
|
||||||
|
|
||||||
|
- Dedupe boundElement ids when container duplicated with alt+drag [#5938](https://github.com/excalidraw/excalidraw/pull/5938)
|
||||||
|
|
||||||
|
- Scale font correctly when using shift [#5935](https://github.com/excalidraw/excalidraw/pull/5935)
|
||||||
|
|
||||||
|
- Always bind to container selected by user [#5880](https://github.com/excalidraw/excalidraw/pull/5880)
|
||||||
|
|
||||||
|
- Fonts not rendered on init if `loadingdone` not fired [#5923](https://github.com/excalidraw/excalidraw/pull/5923)
|
||||||
|
|
||||||
|
- Stop replacing `del` word with `Delete` [#5897](https://github.com/excalidraw/excalidraw/pull/5897)
|
||||||
|
|
||||||
|
- Remove legacy React.render() from the editor [#5893](https://github.com/excalidraw/excalidraw/pull/5893)
|
||||||
|
|
||||||
|
- Allow adding text via enter only for text containers [#5891](https://github.com/excalidraw/excalidraw/pull/5891)
|
||||||
|
|
||||||
|
- Stop font `loadingdone` loop when rendering element SVGs [#5883](https://github.com/excalidraw/excalidraw/pull/5883)
|
||||||
|
|
||||||
|
- Refresh text dimensions only after font load done [#5878](https://github.com/excalidraw/excalidraw/pull/5878)
|
||||||
|
|
||||||
|
- Correctly paste contents parsed by `JSON.parse()` as text. [#5868](https://github.com/excalidraw/excalidraw/pull/5868)
|
||||||
|
|
||||||
|
- SVG element attributes in icons.tsx [#5871](https://github.com/excalidraw/excalidraw/pull/5871)
|
||||||
|
|
||||||
|
- Merge existing text with new when pasted [#5856](https://github.com/excalidraw/excalidraw/pull/5856)
|
||||||
|
|
||||||
|
- Disable FAST_REFRESH to fix live reload [#5852](https://github.com/excalidraw/excalidraw/pull/5852)
|
||||||
|
|
||||||
|
- Paste clipboard contents into unbound text elements [#5849](https://github.com/excalidraw/excalidraw/pull/5849)
|
||||||
|
|
||||||
|
- Compute dimensions of container correctly when text pasted on container [#5845](https://github.com/excalidraw/excalidraw/pull/5845)
|
||||||
|
|
||||||
|
- Line editor points rendering below elements [#5781](https://github.com/excalidraw/excalidraw/pull/5781)
|
||||||
|
|
||||||
|
- Syncing 1-point lines to remote clients [#5677](https://github.com/excalidraw/excalidraw/pull/5677)
|
||||||
|
|
||||||
|
- Incorrectly selecting linear elements on creation while tool-locked [#5785](https://github.com/excalidraw/excalidraw/pull/5785)
|
||||||
|
|
||||||
|
- Corrected typo in toggle theme shortcut [#5813](https://github.com/excalidraw/excalidraw/pull/5813)
|
||||||
|
|
||||||
|
- Hide canvas-modifying UI in view mode [#5815](https://github.com/excalidraw/excalidraw/pull/5815)
|
||||||
|
|
||||||
|
- Fix vertical/horizntal centering icons [#5812](https://github.com/excalidraw/excalidraw/pull/5812)
|
||||||
|
|
||||||
|
- Consistent use of ZOOM_STEP [#5801](https://github.com/excalidraw/excalidraw/pull/5801)
|
||||||
|
|
||||||
|
- Multiple elements resizing regressions [#5586](https://github.com/excalidraw/excalidraw/pull/5586)
|
||||||
|
|
||||||
|
- Changelog typo [#5795](https://github.com/excalidraw/excalidraw/pull/5795)
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Remove unnecessary code [#5933](https://github.com/excalidraw/excalidraw/pull/5933)
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
- Move release scripts to use release branch [#5958](https://github.com/excalidraw/excalidraw/pull/5958)
|
||||||
|
|
||||||
|
- Stops ignoring .env files from docker context so env variables get set during react app build. [#5809](https://github.com/excalidraw/excalidraw/pull/5809)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 0.13.0 (2022-10-27)
|
## 0.13.0 (2022-10-27)
|
||||||
|
|
||||||
### Excalidraw API
|
### Excalidraw API
|
||||||
|
@ -138,9 +138,6 @@ export default function App() {
|
|||||||
console.log("Elements :", elements, "State : ", state)
|
console.log("Elements :", elements, "State : ", state)
|
||||||
}
|
}
|
||||||
onPointerUpdate={(payload) => console.log(payload)}
|
onPointerUpdate={(payload) => console.log(payload)}
|
||||||
onCollabButtonClick={() =>
|
|
||||||
window.alert("You clicked on collab button")
|
|
||||||
}
|
|
||||||
viewModeEnabled={viewModeEnabled}
|
viewModeEnabled={viewModeEnabled}
|
||||||
zenModeEnabled={zenModeEnabled}
|
zenModeEnabled={zenModeEnabled}
|
||||||
gridModeEnabled={gridModeEnabled}
|
gridModeEnabled={gridModeEnabled}
|
||||||
@ -331,7 +328,6 @@ const App = () => {
|
|||||||
onChange: (elements, state) =>
|
onChange: (elements, state) =>
|
||||||
console.log("Elements :", elements, "State : ", state),
|
console.log("Elements :", elements, "State : ", state),
|
||||||
onPointerUpdate: (payload) => console.log(payload),
|
onPointerUpdate: (payload) => console.log(payload),
|
||||||
onCollabButtonClick: () => window.alert("You clicked on collab button"),
|
|
||||||
viewModeEnabled: viewModeEnabled,
|
viewModeEnabled: viewModeEnabled,
|
||||||
zenModeEnabled: zenModeEnabled,
|
zenModeEnabled: zenModeEnabled,
|
||||||
gridModeEnabled: gridModeEnabled,
|
gridModeEnabled: gridModeEnabled,
|
||||||
@ -405,15 +401,14 @@ const App = () => {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
This will only for `Desktop` devices.
|
Footer is only rendered in the desktop view.
|
||||||
|
|
||||||
For `mobile` you will need to render it inside the [MainMenu](#mainmenu). You can use the [`useDevice`](#useDevice) hook to check the type of device, this will be available only inside the `children` of `Excalidraw` component.
|
In the mobile view you can render it inside the [MainMenu](#mainmenu) (later we will expose other ways to customize the UI). You can use the [`useDevice`](#useDevice) hook to check the type of device, this will be available only inside the `children` of `Excalidraw` component.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { useDevice, Footer } from "@excalidraw/excalidraw";
|
import { useDevice, Footer } from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
const MobileFooter = ({
|
const MobileFooter = () => {
|
||||||
}) => {
|
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
if (device.isMobile) {
|
if (device.isMobile) {
|
||||||
return (
|
return (
|
||||||
@ -429,18 +424,21 @@ const MobileFooter = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
<Excalidraw>
|
<Excalidraw>
|
||||||
<MainMenu>
|
<MainMenu>
|
||||||
<MainMenu.Item onSelect={() => window.alert("Item1")}> Item1 </MainMenu.Item>
|
<MainMenu.Item onSelect={() => window.alert("Item1")}>
|
||||||
<MainMenu.Item onSelect={() => window.alert("Item2")}> Item 2 </>
|
Item1
|
||||||
|
</MainMenu.Item>
|
||||||
|
<MainMenu.Item onSelect={() => window.alert("Item2")}>
|
||||||
|
Item2
|
||||||
|
</MainMenu.Item>
|
||||||
<MobileFooter />
|
<MobileFooter />
|
||||||
</MainMenu>
|
</MainMenu>
|
||||||
</Excalidraw>
|
</Excalidraw>;
|
||||||
}
|
};
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You can visit the [example](https://ehlz3.csb.app/) for working demo.
|
You can visit the [example](https://ehlz3.csb.app/) for working demo.
|
||||||
@ -456,11 +454,15 @@ import { MainMenu } from "@excalidraw/excalidraw";
|
|||||||
const App = () => {
|
const App = () => {
|
||||||
<Excalidraw>
|
<Excalidraw>
|
||||||
<MainMenu>
|
<MainMenu>
|
||||||
<MainMenu.Item onSelect={() => window.alert("Item1")}> Item1 </MainMenu.Item>
|
<MainMenu.Item onSelect={() => window.alert("Item1")}>
|
||||||
<MainMenu.Item onSelect={() => window.alert("Item2")}> Item 2 </>
|
Item1
|
||||||
|
</MainMenu.Item>
|
||||||
|
<MainMenu.Item onSelect={() => window.alert("Item2")}>
|
||||||
|
Item2
|
||||||
|
</MainMenu.Item>
|
||||||
</MainMenu>
|
</MainMenu>
|
||||||
</Excalidraw>
|
</Excalidraw>;
|
||||||
}
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
**MainMenu**
|
**MainMenu**
|
||||||
@ -469,28 +471,24 @@ This is the `MainMenu` component which you need to import to render the menu wit
|
|||||||
|
|
||||||
**MainMenu.Item**
|
**MainMenu.Item**
|
||||||
|
|
||||||
To render an item, its recommended to use `MainMenu.Item`.
|
Use this component to render a menu item.
|
||||||
|
|
||||||
| Prop | Type | Required | Default | Description |
|
| Prop | Type | Required | Default | Description |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| `onSelect` | `Function` | Yes | `undefined` | The handler is triggered when the item is selected. |
|
| `onSelect` | `Function` | Yes | `undefined` | The handler is triggered when the item is selected. |
|
||||||
| `children` | `React.ReactNode` | Yes | `undefined` | The content of the menu item |
|
| `children` | `React.ReactNode` | Yes | `undefined` | The content of the menu item |
|
||||||
| `icon` | `JSX.Element` | No | `undefined` | The icon used in the menu item |
|
| `icon` | `JSX.Element` | No | `undefined` | The icon used in the menu item |
|
||||||
| `shortcut` | `string` | No | `undefined` | The shortcut to be shown for the menu item |
|
| `shortcut` | `string` | No | | The keyboard shortcut (label-only, does not affect behavior) |
|
||||||
| `className` | `string` | No | "" | The class names to be added to the menu item |
|
|
||||||
| `style` | `React.CSSProperties` | No | `undefined` | The inline styles to be added to the menu item |
|
|
||||||
| `ariaLabel` | `string` | `undefined` | No | The `aria-label` to be added to the item for accessibility |
|
|
||||||
| `dataTestId` | `string` | `undefined` | No | The `data-testid` to be added to the item. |
|
|
||||||
|
|
||||||
**MainMenu.ItemLink**
|
**MainMenu.ItemLink**
|
||||||
|
|
||||||
To render an item as a link, its recommended to use `MainMenu.ItemLink`.
|
To render an external link in a menu item, you can use this component.
|
||||||
|
|
||||||
**Usage**
|
**Usage**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { MainMenu } from "@excalidraw/excalidraw";
|
import { MainMenu } from "@excalidraw/excalidraw";
|
||||||
const App = () => {
|
const App = () => (
|
||||||
<Excalidraw>
|
<Excalidraw>
|
||||||
<MainMenu>
|
<MainMenu>
|
||||||
<MainMenu.ItemLink href="https://google.com">Google</MainMenu.ItemLink>
|
<MainMenu.ItemLink href="https://google.com">Google</MainMenu.ItemLink>
|
||||||
@ -499,7 +497,7 @@ const App = () => {
|
|||||||
</MainMenu.ItemLink>
|
</MainMenu.ItemLink>
|
||||||
</MainMenu>
|
</MainMenu>
|
||||||
</Excalidraw>;
|
</Excalidraw>;
|
||||||
};
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
| Prop | Type | Required | Default | Description |
|
| Prop | Type | Required | Default | Description |
|
||||||
@ -507,11 +505,7 @@ const App = () => {
|
|||||||
| `href` | `string` | Yes | `undefined` | The `href` attribute to be added to the `anchor` element. |
|
| `href` | `string` | Yes | `undefined` | The `href` attribute to be added to the `anchor` element. |
|
||||||
| `children` | `React.ReactNode` | Yes | `undefined` | The content of the menu item |
|
| `children` | `React.ReactNode` | Yes | `undefined` | The content of the menu item |
|
||||||
| `icon` | `JSX.Element` | No | `undefined` | The icon used in the menu item |
|
| `icon` | `JSX.Element` | No | `undefined` | The icon used in the menu item |
|
||||||
| `shortcut` | `string` | No | `undefined` | The shortcut to be shown for the menu item |
|
| `shortcut` | `string` | No | | The keyboard shortcut (label-only, does not affect behavior) |
|
||||||
| `className` | `string` | No | "" | The class names to be added to the menu item |
|
|
||||||
| `style` | `React.CSSProperties` | No | `undefined` | The inline styles to be added to the menu item |
|
|
||||||
| `ariaLabel` | `string` | No | `undefined` | The `aria-label` to be added to the item for accessibility |
|
|
||||||
| `dataTestId` | `string` | No | `undefined` | The `data-testid` to be added to the item. |
|
|
||||||
|
|
||||||
**MainMenu.ItemCustom**
|
**MainMenu.ItemCustom**
|
||||||
|
|
||||||
@ -521,7 +515,7 @@ To render a custom item, you can use `MainMenu.ItemCustom`.
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
import { MainMenu } from "@excalidraw/excalidraw";
|
import { MainMenu } from "@excalidraw/excalidraw";
|
||||||
const App = () => {
|
const App = () => (
|
||||||
<Excalidraw>
|
<Excalidraw>
|
||||||
<MainMenu>
|
<MainMenu>
|
||||||
<MainMenu.ItemCustom>
|
<MainMenu.ItemCustom>
|
||||||
@ -535,15 +529,12 @@ const App = () => {
|
|||||||
</MainMenu.ItemCustom>
|
</MainMenu.ItemCustom>
|
||||||
</MainMenu>
|
</MainMenu>
|
||||||
</Excalidraw>;
|
</Excalidraw>;
|
||||||
};
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
| Prop | Type | Required | Default | Description |
|
| Prop | Type | Required | Default | Description |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| `children` | `React.ReactNode` | Yes | `undefined` | The content of the menu item |
|
| `children` | `React.ReactNode` | Yes | `undefined` | The content of the menu item |
|
||||||
| `className` | `string` | No | "" | The class names to be added to the menu item |
|
|
||||||
| `style` | `React.CSSProperties` | No | `undefined` | The inline styles to be added to the menu item |
|
|
||||||
| `dataTestId` | `string` | No | `undefined` | The `data-testid` to be added to the item. |
|
|
||||||
|
|
||||||
**MainMenu.DefaultItems**
|
**MainMenu.DefaultItems**
|
||||||
|
|
||||||
@ -551,7 +542,7 @@ For the items which are shown in the menu in [excalidraw.com](https://excalidraw
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
import { MainMenu } from "@excalidraw/excalidraw";
|
import { MainMenu } from "@excalidraw/excalidraw";
|
||||||
const App = () => {
|
const App = () => (
|
||||||
<Excalidraw>
|
<Excalidraw>
|
||||||
<MainMenu>
|
<MainMenu>
|
||||||
<MainMenu.DefaultItems.Socials/>
|
<MainMenu.DefaultItems.Socials/>
|
||||||
@ -560,7 +551,7 @@ const App = () => {
|
|||||||
<MainMenu.Item onSelect={() => window.alert("Item2")}> Item 2 </>
|
<MainMenu.Item onSelect={() => window.alert("Item2")}> Item 2 </>
|
||||||
</MainMenu>
|
</MainMenu>
|
||||||
</Excalidraw>
|
</Excalidraw>
|
||||||
}
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
Here is a [complete list](https://github.com/excalidraw/excalidraw/blob/master/src/components/mainMenu/DefaultItems.tsx) of the default items.
|
Here is a [complete list](https://github.com/excalidraw/excalidraw/blob/master/src/components/mainMenu/DefaultItems.tsx) of the default items.
|
||||||
@ -571,7 +562,7 @@ To Group item in the main menu, you can use `MainMenu.Group`
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
import { MainMenu } from "@excalidraw/excalidraw";
|
import { MainMenu } from "@excalidraw/excalidraw";
|
||||||
const App = () => {
|
const App = () => (
|
||||||
<Excalidraw>
|
<Excalidraw>
|
||||||
<MainMenu>
|
<MainMenu>
|
||||||
<MainMenu.Group title="Excalidraw items">
|
<MainMenu.Group title="Excalidraw items">
|
||||||
@ -584,15 +575,176 @@ const App = () => {
|
|||||||
</MainMenu.Group>
|
</MainMenu.Group>
|
||||||
</MainMenu>
|
</MainMenu>
|
||||||
</Excalidraw>
|
</Excalidraw>
|
||||||
}
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
| Prop | Type | Required | Default | Description |
|
| Prop | Type | Required | Default | Description |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| `children ` | `React.ReactNode` | Yes | `undefined` | The content of the `Menu Group` |
|
| `children ` | `React.ReactNode` | Yes | `undefined` | The content of the `MainMenu.Group` |
|
||||||
| `title` | `string` | No | `undefined` | The `title` for the grouped items |
|
|
||||||
| `className` | `string` | No | "" | The `classname` to be added to the group |
|
### WelcomeScreen
|
||||||
| `style` | `React.CSsSProperties` | No | `undefined` | The inline `styles` to be added to the group |
|
|
||||||
|
When the canvas is empty, Excalidraw shows a welcome "splash" screen with a logo, a few quick action items, and hints explaining what some of the UI buttons do. You can customize the welcome screen by rendering the `WelcomeScreen` component inside your Excalidraw instance.
|
||||||
|
|
||||||
|
You can also disable the welcome screen altogether by setting `UIOptions.welcomeScreen` to `false`.
|
||||||
|
|
||||||
|
**Usage**
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { WelcomScreen } from "@excalidraw/excalidraw";
|
||||||
|
const App = () => (
|
||||||
|
<Excalidraw>
|
||||||
|
<WelcomeScreen>
|
||||||
|
<WelcomeScreen.Center>
|
||||||
|
<WelcomeScreen.Center.Heading>
|
||||||
|
Your data are autosaved to the cloud.
|
||||||
|
</WelcomeScreen.Center.Heading>
|
||||||
|
<WelcomeScreen.Center.Menu>
|
||||||
|
<WelcomeScreen.Center.MenuItem
|
||||||
|
onClick={() => console.log("clicked!")}
|
||||||
|
>
|
||||||
|
Click me!
|
||||||
|
</WelcomeScreen.Center.MenuItem>
|
||||||
|
<WelcomeScreen.Center.MenuItemLink href="https://github.com/excalidraw/excalidraw">
|
||||||
|
Excalidraw GitHub
|
||||||
|
</WelcomeScreen.Center.MenuItemLink>
|
||||||
|
<WelcomeScreen.Center.MenuItemHelp />
|
||||||
|
</WelcomeScreen.Center.Menu>
|
||||||
|
</WelcomeScreen.Center>
|
||||||
|
</WelcomeScreen>
|
||||||
|
</Excalidraw>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
To disable the WelcomeScreen:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { WelcomScreen } from "@excalidraw/excalidraw";
|
||||||
|
const App = () => <Excalidraw UIOptions={{ welcomeScreen: false }} />;
|
||||||
|
```
|
||||||
|
|
||||||
|
**WelcomeScreen**
|
||||||
|
|
||||||
|
If you render the `<WelcomeScreen>` component, you are responsible for rendering the content.
|
||||||
|
|
||||||
|
There are 2 main parts: 1) welcome screen center component, and 2) welcome screen hints.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**WelcomeScreen.Center**
|
||||||
|
|
||||||
|
This is the center piece of the welcome screen, containing the logo, heading, and menu. All three sub-components are optional, and you can render whatever you wish into the center component.
|
||||||
|
|
||||||
|
**WelcomeScreen.Center.Logo**
|
||||||
|
|
||||||
|
By default renders the Excalidraw logo and name. Supply `children` to customize.
|
||||||
|
|
||||||
|
**WelcomeScreen.Center.Heading**
|
||||||
|
|
||||||
|
Supply `children` to change the default message.
|
||||||
|
|
||||||
|
**WelcomeScreen.Center.Menu**
|
||||||
|
|
||||||
|
Wrapper component for the menu items. You can build your menu using the `<WelcomeScreen.Center.MenuItem>` and `<WelcomeScreen.Center.MenuItemLink>` components, render your own, or render one of the default menu items.
|
||||||
|
|
||||||
|
The default menu items are:
|
||||||
|
|
||||||
|
- `<WelcomeScreen.Center.MenuItemHelp/>` - opens the help dialog.
|
||||||
|
- `<WelcomeScreen.Center.MenuItemLoadScene/>` - open the load file dialog.
|
||||||
|
- `<WelcomeScreen.Center.MenuItemLiveCollaborationTrigger/>` - intended to open the live collaboration dialog. Works similarly to [`<LiveCollaborationTrigger>`](#LiveCollaborationTrigger) and you must supply `onSelect()` handler to integrate with your collaboration implementation.
|
||||||
|
|
||||||
|
**Usage**
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { WelcomScreen } from "@excalidraw/excalidraw";
|
||||||
|
const App = () => (
|
||||||
|
<Excalidraw>
|
||||||
|
<WelcomeScreen>
|
||||||
|
<WelcomeScreen.Center>
|
||||||
|
<WelcomeScreen.Center.Menu>
|
||||||
|
<WelcomeScreen.Center.MenuItem
|
||||||
|
onClick={() => console.log("clicked!")}
|
||||||
|
>
|
||||||
|
Click me!
|
||||||
|
</WelcomeScreen.Center.MenuItem>
|
||||||
|
<WelcomeScreen.Center.MenuItemLink href="https://github.com/excalidraw/excalidraw">
|
||||||
|
Excalidraw GitHub
|
||||||
|
</WelcomeScreen.Center.MenuItemLink>
|
||||||
|
<WelcomeScreen.Center.MenuItemHelp />
|
||||||
|
</WelcomeScreen.Center.Menu>
|
||||||
|
</WelcomeScreen.Center>
|
||||||
|
</WelcomeScreen>
|
||||||
|
</Excalidraw>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**WelcomeScreen.Center.MenuItem**
|
||||||
|
|
||||||
|
Use this component to render a menu item.
|
||||||
|
|
||||||
|
| Prop | Type | Required | Default | Description |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `onSelect` | `Function` | Yes | | The handler is triggered when the item is selected. |
|
||||||
|
| `children` | `React.ReactNode` | Yes | | The content of the menu item |
|
||||||
|
| `icon` | `JSX.Element` | No | | The icon used in the menu item |
|
||||||
|
| `shortcut` | `string` | No | | The keyboard shortcut (label-only, does not affect behavior) |
|
||||||
|
|
||||||
|
**WelcomeScreen.Center.MenuItemLink**
|
||||||
|
|
||||||
|
To render an external link in a menu item, you can use this component.
|
||||||
|
|
||||||
|
| Prop | Type | Required | Default | Description |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `href` | `string` | Yes | | The `href` attribute to be added to the `anchor` element. |
|
||||||
|
| `children` | `React.ReactNode` | Yes | | The content of the menu item |
|
||||||
|
| `icon` | `JSX.Element` | No | | The icon used in the menu item |
|
||||||
|
| `shortcut` | `string` | No | | The keyboard shortcut (label-only, does not affect behavior) |
|
||||||
|
|
||||||
|
**WelcomeScreen.Hints**
|
||||||
|
|
||||||
|
These subcomponents render the UI hints. Text of each hint can be customized by supplying `children`.
|
||||||
|
|
||||||
|
**WelcomeScreen.Hints.Menu**
|
||||||
|
|
||||||
|
Hint for the main menu. Supply `children` to customize the hint text.
|
||||||
|
|
||||||
|
**WelcomeScreen.Hints.Toolbar**
|
||||||
|
|
||||||
|
Hint for the toolbar. Supply `children` to customize the hint text.
|
||||||
|
|
||||||
|
**WelcomeScreen.Hints.Help**
|
||||||
|
|
||||||
|
Hint for the help dialog. Supply `children` to customize the hint text.
|
||||||
|
|
||||||
|
### LiveCollaborationTrigger
|
||||||
|
|
||||||
|
If you implement live collaboration support and want to expose the same UI button as on excalidraw.com, you can render the `<LiveCollaborationTrigger>` component using the [renderTopRightUI](#rendertoprightui) prop. You'll need to supply `onSelect()` to handle opening of your collaboration dialog, but the button will display current `appState.collaborators` count for you.
|
||||||
|
|
||||||
|
| Prop | Type | Required | Default | Description |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `onSelect` | `() => any` | Yes | | Handler called when the user click on the button |
|
||||||
|
| `isCollaborating` | `boolean` | Yes | false | Whether live collaboration session is in effect. Modifies button style. |
|
||||||
|
|
||||||
|
**Usage**
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { LiveCollaborationTrigger } from "@excalidraw/excalidraw";
|
||||||
|
const App = () => (
|
||||||
|
<Excalidraw
|
||||||
|
renderTopRightUI={(isMobile) => {
|
||||||
|
if (isMobile) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<LiveCollaborationTrigger
|
||||||
|
isCollaborating={isCollaborating}
|
||||||
|
onSelect={() => setCollabDialogShown(true)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
### Props
|
### Props
|
||||||
|
|
||||||
@ -601,7 +753,6 @@ const App = () => {
|
|||||||
| [`onChange`](#onChange) | Function | | This callback is triggered whenever the component updates due to any change. This callback will receive the excalidraw elements and the current app state. |
|
| [`onChange`](#onChange) | Function | | This callback is triggered whenever the component updates due to any change. This callback will receive the excalidraw elements and the current app state. |
|
||||||
| [`initialData`](#initialData) | <code>{elements?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a>, appState?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState<a> } </code> | null | The initial data with which app loads. |
|
| [`initialData`](#initialData) | <code>{elements?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a>, appState?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState<a> } </code> | null | The initial data with which app loads. |
|
||||||
| [`ref`](#ref) | [`createRef`](https://reactjs.org/docs/refs-and-the-dom.html#creating-refs) | [`useRef`](https://reactjs.org/docs/hooks-reference.html#useref) | [`callbackRef`](https://reactjs.org/docs/refs-and-the-dom.html#callback-refs) | <code>{ current: { readyPromise: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L317">resolvablePromise</a> } }</code> | | Ref to be passed to Excalidraw |
|
| [`ref`](#ref) | [`createRef`](https://reactjs.org/docs/refs-and-the-dom.html#creating-refs) | [`useRef`](https://reactjs.org/docs/hooks-reference.html#useref) | [`callbackRef`](https://reactjs.org/docs/refs-and-the-dom.html#callback-refs) | <code>{ current: { readyPromise: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L317">resolvablePromise</a> } }</code> | | Ref to be passed to Excalidraw |
|
||||||
| [`onCollabButtonClick`](#onCollabButtonClick) | Function | | Callback to be triggered when the collab button is clicked |
|
|
||||||
| [`isCollaborating`](#isCollaborating) | `boolean` | | This implies if the app is in collaboration mode |
|
| [`isCollaborating`](#isCollaborating) | `boolean` | | This implies if the app is in collaboration mode |
|
||||||
| [`onPointerUpdate`](#onPointerUpdate) | Function | | Callback triggered when mouse pointer is updated. |
|
| [`onPointerUpdate`](#onPointerUpdate) | Function | | Callback triggered when mouse pointer is updated. |
|
||||||
| [`langCode`](#langCode) | string | `en` | Language code string |
|
| [`langCode`](#langCode) | string | `en` | Language code string |
|
||||||
@ -775,10 +926,6 @@ You can use this function to update the library. It accepts the below attributes
|
|||||||
|
|
||||||
Adds supplied files data to the `appState.files` cache on top of existing files present in the cache.
|
Adds supplied files data to the `appState.files` cache on top of existing files present in the cache.
|
||||||
|
|
||||||
#### `onCollabButtonClick`
|
|
||||||
|
|
||||||
This callback is triggered when clicked on the collab button in excalidraw. If not supplied, the collab dialog button is not rendered.
|
|
||||||
|
|
||||||
#### `isCollaborating`
|
#### `isCollaborating`
|
||||||
|
|
||||||
This prop indicates if the app is in collaboration mode.
|
This prop indicates if the app is in collaboration mode.
|
||||||
@ -1565,8 +1712,7 @@ This hook can be used to check the type of device which is being used. It can on
|
|||||||
```js
|
```js
|
||||||
import { useDevice, Footer } from "@excalidraw/excalidraw";
|
import { useDevice, Footer } from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
const MobileFooter = ({
|
const MobileFooter = () => {
|
||||||
}) => {
|
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
if (device.isMobile) {
|
if (device.isMobile) {
|
||||||
return (
|
return (
|
||||||
|
@ -86,24 +86,13 @@ const {
|
|||||||
Sidebar,
|
Sidebar,
|
||||||
Footer,
|
Footer,
|
||||||
MainMenu,
|
MainMenu,
|
||||||
|
LiveCollaborationTrigger,
|
||||||
} = window.ExcalidrawLib;
|
} = window.ExcalidrawLib;
|
||||||
|
|
||||||
const COMMENT_ICON_DIMENSION = 32;
|
const COMMENT_ICON_DIMENSION = 32;
|
||||||
const COMMENT_INPUT_HEIGHT = 50;
|
const COMMENT_INPUT_HEIGHT = 50;
|
||||||
const COMMENT_INPUT_WIDTH = 150;
|
const COMMENT_INPUT_WIDTH = 150;
|
||||||
|
|
||||||
const renderTopRightUI = () => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={() => alert("This is dummy top right UI")}
|
|
||||||
style={{ height: "2.5rem" }}
|
|
||||||
>
|
|
||||||
{" "}
|
|
||||||
Click me{" "}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const appRef = useRef<any>(null);
|
const appRef = useRef<any>(null);
|
||||||
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
||||||
@ -164,6 +153,28 @@ export default function App() {
|
|||||||
fetchData();
|
fetchData();
|
||||||
}, [excalidrawAPI]);
|
}, [excalidrawAPI]);
|
||||||
|
|
||||||
|
const renderTopRightUI = (isMobile: boolean) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!isMobile && (
|
||||||
|
<LiveCollaborationTrigger
|
||||||
|
isCollaborating={isCollaborating}
|
||||||
|
onSelect={() => {
|
||||||
|
window.alert("Collab dialog clicked");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => alert("This is dummy top right UI")}
|
||||||
|
style={{ height: "2.5rem" }}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
Click me{" "}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const loadSceneOrLibrary = async () => {
|
const loadSceneOrLibrary = async () => {
|
||||||
const file = await fileOpen({ description: "Excalidraw or library file" });
|
const file = await fileOpen({ description: "Excalidraw or library file" });
|
||||||
const contents = await loadSceneOrLibraryFromBlob(file, null, null);
|
const contents = await loadSceneOrLibraryFromBlob(file, null, null);
|
||||||
@ -505,12 +516,10 @@ export default function App() {
|
|||||||
<MainMenu.DefaultItems.SaveAsImage />
|
<MainMenu.DefaultItems.SaveAsImage />
|
||||||
<MainMenu.DefaultItems.Export />
|
<MainMenu.DefaultItems.Export />
|
||||||
<MainMenu.Separator />
|
<MainMenu.Separator />
|
||||||
{isCollaborating && (
|
<MainMenu.DefaultItems.LiveCollaborationTrigger
|
||||||
<MainMenu.DefaultItems.LiveCollaboration
|
|
||||||
onSelect={() => window.alert("You clicked on collab button")}
|
|
||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
|
onSelect={() => window.alert("You clicked on collab button")}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
<MainMenu.Group title="Excalidraw links">
|
<MainMenu.Group title="Excalidraw links">
|
||||||
<MainMenu.DefaultItems.Socials />
|
<MainMenu.DefaultItems.Socials />
|
||||||
</MainMenu.Group>
|
</MainMenu.Group>
|
||||||
@ -524,6 +533,7 @@ export default function App() {
|
|||||||
</button>
|
</button>
|
||||||
</MainMenu.ItemCustom>
|
</MainMenu.ItemCustom>
|
||||||
<MainMenu.DefaultItems.Help />
|
<MainMenu.DefaultItems.Help />
|
||||||
|
|
||||||
{excalidrawAPI && <MobileFooter excalidrawAPI={excalidrawAPI} />}
|
{excalidrawAPI && <MobileFooter excalidrawAPI={excalidrawAPI} />}
|
||||||
</MainMenu>
|
</MainMenu>
|
||||||
);
|
);
|
||||||
@ -693,9 +703,6 @@ export default function App() {
|
|||||||
button: "down" | "up";
|
button: "down" | "up";
|
||||||
pointersMap: Gesture["pointers"];
|
pointersMap: Gesture["pointers"];
|
||||||
}) => setPointerData(payload)}
|
}) => setPointerData(payload)}
|
||||||
onCollabButtonClick={() =>
|
|
||||||
window.alert("You clicked on collab button")
|
|
||||||
}
|
|
||||||
viewModeEnabled={viewModeEnabled}
|
viewModeEnabled={viewModeEnabled}
|
||||||
zenModeEnabled={zenModeEnabled}
|
zenModeEnabled={zenModeEnabled}
|
||||||
gridModeEnabled={gridModeEnabled}
|
gridModeEnabled={gridModeEnabled}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { ExcalidrawImperativeAPI } from "../../../types";
|
import { ExcalidrawImperativeAPI } from "../../../types";
|
||||||
import { MIME_TYPES } from "../entry";
|
import { MIME_TYPES } from "../entry";
|
||||||
|
import { Button } from "../../../components/Button";
|
||||||
|
|
||||||
const COMMENT_SVG = (
|
const COMMENT_SVG = (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -23,6 +25,14 @@ const CustomFooter = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Button
|
||||||
|
onSelect={() => alert("General Kenobi!")}
|
||||||
|
className="you are a bold one"
|
||||||
|
style={{ marginLeft: "1rem" }}
|
||||||
|
title="Hello there!"
|
||||||
|
>
|
||||||
|
{COMMENT_SVG}
|
||||||
|
</Button>
|
||||||
<button
|
<button
|
||||||
className="custom-element"
|
className="custom-element"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, forwardRef } from "react";
|
import React, { useEffect, forwardRef } from "react";
|
||||||
import { InitializeApp } from "../../components/InitializeApp";
|
import { InitializeApp } from "../../components/InitializeApp";
|
||||||
import App from "../../components/App";
|
import App from "../../components/App";
|
||||||
|
import { isShallowEqual } from "../../utils";
|
||||||
|
|
||||||
import "../../css/app.scss";
|
import "../../css/app.scss";
|
||||||
import "../../css/styles.scss";
|
import "../../css/styles.scss";
|
||||||
@ -11,14 +12,15 @@ import { DEFAULT_UI_OPTIONS } from "../../constants";
|
|||||||
import { Provider } from "jotai";
|
import { Provider } from "jotai";
|
||||||
import { jotaiScope, jotaiStore } from "../../jotai";
|
import { jotaiScope, jotaiStore } from "../../jotai";
|
||||||
import Footer from "../../components/footer/FooterCenter";
|
import Footer from "../../components/footer/FooterCenter";
|
||||||
import MainMenu from "../../components/mainMenu/MainMenu";
|
import MainMenu from "../../components/main-menu/MainMenu";
|
||||||
|
import WelcomeScreen from "../../components/welcome-screen/WelcomeScreen";
|
||||||
|
import LiveCollaborationTrigger from "../../components/live-collaboration/LiveCollaborationTrigger";
|
||||||
|
|
||||||
const ExcalidrawBase = (props: ExcalidrawProps) => {
|
const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||||
const {
|
const {
|
||||||
onChange,
|
onChange,
|
||||||
initialData,
|
initialData,
|
||||||
excalidrawRef,
|
excalidrawRef,
|
||||||
onCollabButtonClick,
|
|
||||||
isCollaborating = false,
|
isCollaborating = false,
|
||||||
onPointerUpdate,
|
onPointerUpdate,
|
||||||
renderTopRightUI,
|
renderTopRightUI,
|
||||||
@ -51,6 +53,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||||||
...DEFAULT_UI_OPTIONS.canvasActions,
|
...DEFAULT_UI_OPTIONS.canvasActions,
|
||||||
...canvasActions,
|
...canvasActions,
|
||||||
},
|
},
|
||||||
|
welcomeScreen: props.UIOptions?.welcomeScreen ?? true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (canvasActions?.export) {
|
if (canvasActions?.export) {
|
||||||
@ -91,7 +94,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
initialData={initialData}
|
initialData={initialData}
|
||||||
excalidrawRef={excalidrawRef}
|
excalidrawRef={excalidrawRef}
|
||||||
onCollabButtonClick={onCollabButtonClick}
|
|
||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
onPointerUpdate={onPointerUpdate}
|
onPointerUpdate={onPointerUpdate}
|
||||||
renderTopRightUI={renderTopRightUI}
|
renderTopRightUI={renderTopRightUI}
|
||||||
@ -128,6 +130,11 @@ const areEqual = (
|
|||||||
prevProps: PublicExcalidrawProps,
|
prevProps: PublicExcalidrawProps,
|
||||||
nextProps: PublicExcalidrawProps,
|
nextProps: PublicExcalidrawProps,
|
||||||
) => {
|
) => {
|
||||||
|
// short-circuit early
|
||||||
|
if (prevProps.children !== nextProps.children) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
initialData: prevInitialData,
|
initialData: prevInitialData,
|
||||||
UIOptions: prevUIOptions = {},
|
UIOptions: prevUIOptions = {},
|
||||||
@ -156,7 +163,7 @@ const areEqual = (
|
|||||||
const canvasOptionKeys = Object.keys(
|
const canvasOptionKeys = Object.keys(
|
||||||
prevUIOptions.canvasActions!,
|
prevUIOptions.canvasActions!,
|
||||||
) as (keyof Partial<typeof DEFAULT_UI_OPTIONS.canvasActions>)[];
|
) as (keyof Partial<typeof DEFAULT_UI_OPTIONS.canvasActions>)[];
|
||||||
canvasOptionKeys.every((key) => {
|
return canvasOptionKeys.every((key) => {
|
||||||
if (
|
if (
|
||||||
key === "export" &&
|
key === "export" &&
|
||||||
prevUIOptions?.canvasActions?.export &&
|
prevUIOptions?.canvasActions?.export &&
|
||||||
@ -173,16 +180,10 @@ const areEqual = (
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return true;
|
return prevUIOptions[key] === nextUIOptions[key];
|
||||||
});
|
});
|
||||||
|
|
||||||
const prevKeys = Object.keys(prevProps) as (keyof typeof prev)[];
|
return isUIOptionsSame && isShallowEqual(prev, next);
|
||||||
const nextKeys = Object.keys(nextProps) as (keyof typeof next)[];
|
|
||||||
return (
|
|
||||||
isUIOptionsSame &&
|
|
||||||
prevKeys.length === nextKeys.length &&
|
|
||||||
prevKeys.every((key) => prev[key] === next[key])
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const forwardedRefComp = forwardRef<
|
const forwardedRefComp = forwardRef<
|
||||||
@ -239,6 +240,9 @@ export {
|
|||||||
} from "../../utils";
|
} from "../../utils";
|
||||||
|
|
||||||
export { Sidebar } from "../../components/Sidebar/Sidebar";
|
export { Sidebar } from "../../components/Sidebar/Sidebar";
|
||||||
|
export { Button } from "../../components/Button";
|
||||||
export { Footer };
|
export { Footer };
|
||||||
export { MainMenu };
|
export { MainMenu };
|
||||||
export { useDevice } from "../../components/App";
|
export { useDevice } from "../../components/App";
|
||||||
|
export { WelcomeScreen };
|
||||||
|
export { LiveCollaborationTrigger };
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@excalidraw/excalidraw",
|
"name": "@excalidraw/excalidraw",
|
||||||
"version": "0.13.0",
|
"version": "0.14.1",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"types": "types/packages/excalidraw/index.d.ts",
|
"types": "types/packages/excalidraw/index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
|
BIN
src/packages/excalidraw/welcome-screen-overview.png
Normal file
BIN
src/packages/excalidraw/welcome-screen-overview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 101 KiB |
@ -713,22 +713,8 @@ const drawElementFromCanvas = (
|
|||||||
const cx = ((x1 + x2) / 2 + renderConfig.scrollX) * window.devicePixelRatio;
|
const cx = ((x1 + x2) / 2 + renderConfig.scrollX) * window.devicePixelRatio;
|
||||||
const cy = ((y1 + y2) / 2 + renderConfig.scrollY) * window.devicePixelRatio;
|
const cy = ((y1 + y2) / 2 + renderConfig.scrollY) * window.devicePixelRatio;
|
||||||
|
|
||||||
const _isPendingImageElement = isPendingImageElement(element, renderConfig);
|
|
||||||
|
|
||||||
const scaleXFactor =
|
|
||||||
"scale" in elementWithCanvas.element && !_isPendingImageElement
|
|
||||||
? elementWithCanvas.element.scale[0]
|
|
||||||
: 1;
|
|
||||||
const scaleYFactor =
|
|
||||||
"scale" in elementWithCanvas.element && !_isPendingImageElement
|
|
||||||
? elementWithCanvas.element.scale[1]
|
|
||||||
: 1;
|
|
||||||
|
|
||||||
context.save();
|
context.save();
|
||||||
context.scale(
|
context.scale(1 / window.devicePixelRatio, 1 / window.devicePixelRatio);
|
||||||
(1 / window.devicePixelRatio) * scaleXFactor,
|
|
||||||
(1 / window.devicePixelRatio) * scaleYFactor,
|
|
||||||
);
|
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
|
|
||||||
if (isArrowElement(element) && boundTextElement) {
|
if (isArrowElement(element) && boundTextElement) {
|
||||||
@ -793,7 +779,7 @@ const drawElementFromCanvas = (
|
|||||||
zoom,
|
zoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
context.translate(cx * scaleXFactor, cy * scaleYFactor);
|
context.translate(cx, cy);
|
||||||
context.drawImage(
|
context.drawImage(
|
||||||
tempCanvas,
|
tempCanvas,
|
||||||
(-(x2 - x1) / 2) * window.devicePixelRatio - offsetX / zoom - padding,
|
(-(x2 - x1) / 2) * window.devicePixelRatio - offsetX / zoom - padding,
|
||||||
@ -802,15 +788,30 @@ const drawElementFromCanvas = (
|
|||||||
tempCanvas.height / zoom,
|
tempCanvas.height / zoom,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
context.translate(cx * scaleXFactor, cy * scaleYFactor);
|
// we translate context to element center so that rotation and scale
|
||||||
|
// originates from the element center
|
||||||
|
context.translate(cx, cy);
|
||||||
|
|
||||||
context.rotate(element.angle * scaleXFactor * scaleYFactor);
|
context.rotate(element.angle);
|
||||||
|
|
||||||
|
if (
|
||||||
|
"scale" in elementWithCanvas.element &&
|
||||||
|
!isPendingImageElement(element, renderConfig)
|
||||||
|
) {
|
||||||
|
context.scale(
|
||||||
|
elementWithCanvas.element.scale[0],
|
||||||
|
elementWithCanvas.element.scale[1],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// revert afterwards we don't have account for it during drawing
|
||||||
|
context.translate(-cx, -cy);
|
||||||
|
|
||||||
context.drawImage(
|
context.drawImage(
|
||||||
elementWithCanvas.canvas!,
|
elementWithCanvas.canvas!,
|
||||||
(-(x2 - x1) / 2) * window.devicePixelRatio -
|
(x1 + renderConfig.scrollX) * window.devicePixelRatio -
|
||||||
(padding * elementWithCanvas.canvasZoom) / elementWithCanvas.canvasZoom,
|
(padding * elementWithCanvas.canvasZoom) / elementWithCanvas.canvasZoom,
|
||||||
(-(y2 - y1) / 2) * window.devicePixelRatio -
|
(y1 + renderConfig.scrollY) * window.devicePixelRatio -
|
||||||
(padding * elementWithCanvas.canvasZoom) / elementWithCanvas.canvasZoom,
|
(padding * elementWithCanvas.canvasZoom) / elementWithCanvas.canvasZoom,
|
||||||
elementWithCanvas.canvas!.width / elementWithCanvas.canvasZoom,
|
elementWithCanvas.canvas!.width / elementWithCanvas.canvasZoom,
|
||||||
elementWithCanvas.canvas!.height / elementWithCanvas.canvasZoom,
|
elementWithCanvas.canvas!.height / elementWithCanvas.canvasZoom,
|
||||||
@ -905,9 +906,6 @@ export const renderElement = (
|
|||||||
}
|
}
|
||||||
context.save();
|
context.save();
|
||||||
context.translate(cx, cy);
|
context.translate(cx, cy);
|
||||||
if (element.type === "image") {
|
|
||||||
context.scale(element.scale[0], element.scale[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldResetImageFilter(element, renderConfig)) {
|
if (shouldResetImageFilter(element, renderConfig)) {
|
||||||
context.filter = "none";
|
context.filter = "none";
|
||||||
@ -973,6 +971,12 @@ export const renderElement = (
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
context.rotate(element.angle);
|
context.rotate(element.angle);
|
||||||
|
|
||||||
|
if (element.type === "image") {
|
||||||
|
// note: scale must be applied *after* rotating
|
||||||
|
context.scale(element.scale[0], element.scale[1]);
|
||||||
|
}
|
||||||
|
|
||||||
context.translate(-shiftX, -shiftY);
|
context.translate(-shiftX, -shiftY);
|
||||||
drawElementOnCanvas(element, rc, context, renderConfig);
|
drawElementOnCanvas(element, rc, context, renderConfig);
|
||||||
}
|
}
|
||||||
|
@ -41,8 +41,8 @@ export const centerScrollOn = ({
|
|||||||
zoom: Zoom;
|
zoom: Zoom;
|
||||||
}) => {
|
}) => {
|
||||||
return {
|
return {
|
||||||
scrollX: (viewportDimensions.width / 2) * (1 / zoom.value) - scenePoint.x,
|
scrollX: viewportDimensions.width / 2 / zoom.value - scenePoint.x,
|
||||||
scrollY: (viewportDimensions.height / 2) * (1 / zoom.value) - scenePoint.y,
|
scrollY: viewportDimensions.height / 2 / zoom.value - scenePoint.y,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
68
src/types.ts
68
src/types.ts
@ -287,7 +287,6 @@ export interface ExcalidrawProps {
|
|||||||
| null
|
| null
|
||||||
| Promise<ExcalidrawInitialDataState | null>;
|
| Promise<ExcalidrawInitialDataState | null>;
|
||||||
excalidrawRef?: ForwardRef<ExcalidrawAPIRefValue>;
|
excalidrawRef?: ForwardRef<ExcalidrawAPIRefValue>;
|
||||||
onCollabButtonClick?: () => void;
|
|
||||||
isCollaborating?: boolean;
|
isCollaborating?: boolean;
|
||||||
onPointerUpdate?: (payload: {
|
onPointerUpdate?: (payload: {
|
||||||
pointer: { x: number; y: number };
|
pointer: { x: number; y: number };
|
||||||
@ -313,10 +312,7 @@ export interface ExcalidrawProps {
|
|||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => JSX.Element;
|
) => JSX.Element;
|
||||||
UIOptions?: {
|
UIOptions?: Partial<UIOptions>;
|
||||||
dockedSidebarBreakpoint?: number;
|
|
||||||
canvasActions?: CanvasActions;
|
|
||||||
};
|
|
||||||
detectScroll?: boolean;
|
detectScroll?: boolean;
|
||||||
handleKeyboardGlobally?: boolean;
|
handleKeyboardGlobally?: boolean;
|
||||||
onLibraryChange?: (libraryItems: LibraryItems) => void | Promise<any>;
|
onLibraryChange?: (libraryItems: LibraryItems) => void | Promise<any>;
|
||||||
@ -373,23 +369,31 @@ export type ExportOpts = {
|
|||||||
// truthiness value will determine whether the action is rendered or not
|
// truthiness value will determine whether the action is rendered or not
|
||||||
// (see manager renderAction). We also override canvasAction values in
|
// (see manager renderAction). We also override canvasAction values in
|
||||||
// excalidraw package index.tsx.
|
// excalidraw package index.tsx.
|
||||||
type CanvasActions = {
|
type CanvasActions = Partial<{
|
||||||
changeViewBackgroundColor?: boolean;
|
changeViewBackgroundColor: boolean;
|
||||||
clearCanvas?: boolean;
|
clearCanvas: boolean;
|
||||||
export?: false | ExportOpts;
|
export: false | ExportOpts;
|
||||||
loadScene?: boolean;
|
loadScene: boolean;
|
||||||
saveToActiveFile?: boolean;
|
saveToActiveFile: boolean;
|
||||||
toggleTheme?: boolean | null;
|
toggleTheme: boolean | null;
|
||||||
saveAsImage?: boolean;
|
saveAsImage: boolean;
|
||||||
};
|
}>;
|
||||||
|
|
||||||
|
type UIOptions = Partial<{
|
||||||
|
dockedSidebarBreakpoint: number;
|
||||||
|
welcomeScreen: boolean;
|
||||||
|
canvasActions: CanvasActions;
|
||||||
|
}>;
|
||||||
|
|
||||||
export type AppProps = Merge<
|
export type AppProps = Merge<
|
||||||
ExcalidrawProps,
|
ExcalidrawProps,
|
||||||
{
|
{
|
||||||
UIOptions: {
|
UIOptions: Merge<
|
||||||
|
MarkRequired<UIOptions, "welcomeScreen">,
|
||||||
|
{
|
||||||
canvasActions: Required<CanvasActions> & { export: ExportOpts };
|
canvasActions: Required<CanvasActions> & { export: ExportOpts };
|
||||||
dockedSidebarBreakpoint?: number;
|
}
|
||||||
};
|
>;
|
||||||
detectScroll: boolean;
|
detectScroll: boolean;
|
||||||
handleKeyboardGlobally: boolean;
|
handleKeyboardGlobally: boolean;
|
||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
@ -518,7 +522,31 @@ export type Device = Readonly<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type UIChildrenComponents = {
|
export type UIChildrenComponents = {
|
||||||
[k in "FooterCenter" | "Menu"]?:
|
[k in "FooterCenter" | "Menu" | "WelcomeScreen"]?: React.ReactElement<
|
||||||
| React.ReactPortal
|
{ children?: React.ReactNode },
|
||||||
| React.ReactElement<unknown, string | React.JSXElementConstructor<any>>;
|
React.JSXElementConstructor<any>
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UIWelcomeScreenComponents = {
|
||||||
|
[k in
|
||||||
|
| "Center"
|
||||||
|
| "MenuHint"
|
||||||
|
| "ToolbarHint"
|
||||||
|
| "HelpHint"]?: React.ReactElement<
|
||||||
|
{ children?: React.ReactNode },
|
||||||
|
React.JSXElementConstructor<any>
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UIWelcomeScreenCenterComponents = {
|
||||||
|
[k in
|
||||||
|
| "Logo"
|
||||||
|
| "Heading"
|
||||||
|
| "Menu"
|
||||||
|
| "MenuItemLoadScene"
|
||||||
|
| "MenuItemHelp"]?: React.ReactElement<
|
||||||
|
{ children?: React.ReactNode },
|
||||||
|
React.JSXElementConstructor<any>
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
56
src/utils.ts
56
src/utils.ts
@ -352,9 +352,8 @@ export const viewportCoordsToSceneCoords = (
|
|||||||
scrollY: number;
|
scrollY: number;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const invScale = 1 / zoom.value;
|
const x = (clientX - offsetLeft) / zoom.value - scrollX;
|
||||||
const x = (clientX - offsetLeft) * invScale - scrollX;
|
const y = (clientY - offsetTop) / zoom.value - scrollY;
|
||||||
const y = (clientY - offsetTop) * invScale - scrollY;
|
|
||||||
|
|
||||||
return { x, y };
|
return { x, y };
|
||||||
};
|
};
|
||||||
@ -688,25 +687,56 @@ export const queryFocusableElements = (container: HTMLElement | null) => {
|
|||||||
: [];
|
: [];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReactChildrenToObject = <
|
/**
|
||||||
T extends {
|
* Partitions React children into named components and the rest of children.
|
||||||
[k in string]?:
|
*
|
||||||
| React.ReactPortal
|
* Returns known children as a dictionary of react children keyed by their
|
||||||
| React.ReactElement<unknown, string | React.JSXElementConstructor<any>>;
|
* displayName, and the rest children as an array.
|
||||||
|
*
|
||||||
|
* NOTE all named react components are included in the dictionary, irrespective
|
||||||
|
* of the supplied type parameter. This means you may be throwing away
|
||||||
|
* children that you aren't expecting, but should nonetheless be rendered.
|
||||||
|
* To guard against this (provided you care about the rest children at all),
|
||||||
|
* supply a second parameter with an object with keys of the expected children.
|
||||||
|
*/
|
||||||
|
export const getReactChildren = <
|
||||||
|
KnownChildren extends {
|
||||||
|
[k in string]?: React.ReactNode;
|
||||||
},
|
},
|
||||||
>(
|
>(
|
||||||
children: React.ReactNode,
|
children: React.ReactNode,
|
||||||
|
expectedComponents?: Record<keyof KnownChildren, any>,
|
||||||
) => {
|
) => {
|
||||||
return React.Children.toArray(children).reduce((acc, child) => {
|
const restChildren: React.ReactNode[] = [];
|
||||||
|
|
||||||
|
const knownChildren = React.Children.toArray(children).reduce(
|
||||||
|
(acc, child) => {
|
||||||
if (
|
if (
|
||||||
React.isValidElement(child) &&
|
React.isValidElement(child) &&
|
||||||
typeof child.type !== "string" &&
|
(!expectedComponents ||
|
||||||
//@ts-ignore
|
((child.type as any).displayName as string) in expectedComponents)
|
||||||
child?.type.displayName
|
|
||||||
) {
|
) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
acc[child.type.displayName] = child;
|
acc[child.type.displayName] = child;
|
||||||
|
} else {
|
||||||
|
restChildren.push(child);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Partial<T>);
|
},
|
||||||
|
{} as Partial<KnownChildren>,
|
||||||
|
);
|
||||||
|
|
||||||
|
return [knownChildren, restChildren] as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isShallowEqual = <T extends Record<string, any>>(
|
||||||
|
objA: T,
|
||||||
|
objB: T,
|
||||||
|
) => {
|
||||||
|
const aKeys = Object.keys(objA);
|
||||||
|
const bKeys = Object.keys(objA);
|
||||||
|
if (aKeys.length !== bKeys.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return aKeys.every((key) => objA[key] === objB[key]);
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user