mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Merge branch 'master' of github.com:excalidraw/excalidraw into cleanipp
* 'master' of github.com:excalidraw/excalidraw: (37 commits) feat: Add toast (#2772) docs: Update readme with documentation (#2788) fix: allow text-selecting in dialogs & reset cursor (#2783) chore: Update translations from Crowdin (#2742) fix: broken Individuals link (#2782) refactor: Converting span to kbd tag (#2774) fix: don't render due to zoom after unmount (#2779) fix: Track the chart type correctly (#2773) chore(deps-dev): bump terser-webpack-plugin in /src/packages/excalidraw (#2750) chore(deps-dev): bump webpack in /src/packages/utils (#2768) fix: delay version logging & prevent duplicates (#2770) chore(deps-dev): bump webpack in /src/packages/excalidraw (#2769) chore(deps-dev): bump ts-loader in /src/packages/excalidraw (#2749) chore(deps-dev): bump ts-loader in /src/packages/utils (#2753) chore(deps): bump nanoid from 2.1.11 to 3.1.20 (#2581) feat: Track current version (#2731) chore(actions): Use cancel workflow action (#2763) chore(deps): bump @testing-library/react from 11.2.2 to 11.2.3 (#2755) chore(deps-dev): bump firebase-tools from 9.1.0 to 9.1.2 (#2761) chore(deps-dev): bump eslint-plugin-prettier from 3.3.0 to 3.3.1 (#2754) ...
This commit is contained in:
commit
1eb8920bc3
115 changed files with 2445 additions and 4938 deletions
|
@ -1,23 +1,22 @@
|
|||
import React from "react";
|
||||
import { AppState, Zoom } from "../types";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import {
|
||||
hasBackground,
|
||||
hasStroke,
|
||||
canChangeSharpness,
|
||||
hasText,
|
||||
canHaveArrowheads,
|
||||
getTargetElements,
|
||||
hasBackground,
|
||||
hasStroke,
|
||||
hasText,
|
||||
} from "../scene";
|
||||
import { t } from "../i18n";
|
||||
import { SHAPES } from "../shapes";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { AppState, Zoom } from "../types";
|
||||
import { capitalizeString, isTransparent, setCursorForShape } from "../utils";
|
||||
import Stack from "./Stack";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { trackEvent, EVENT_SHAPE, EVENT_DIALOG } from "../analytics";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
|
||||
export const SelectedShapeActions = ({
|
||||
appState,
|
||||
|
@ -181,7 +180,6 @@ export const ShapesSwitcher = ({
|
|||
aria-keyshortcuts={shortcut}
|
||||
data-testid={value}
|
||||
onChange={() => {
|
||||
trackEvent(EVENT_SHAPE, value, "toolbar");
|
||||
setAppState({
|
||||
elementType: value,
|
||||
multiElement: null,
|
||||
|
@ -203,9 +201,6 @@ export const ShapesSwitcher = ({
|
|||
title={`${capitalizeString(t("toolBar.library"))} — 9`}
|
||||
aria-label={capitalizeString(t("toolBar.library"))}
|
||||
onClick={() => {
|
||||
if (!isLibraryOpen) {
|
||||
trackEvent(EVENT_DIALOG, "library");
|
||||
}
|
||||
setAppState({ isLibraryOpen: !isLibraryOpen });
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -8,12 +8,7 @@ import { createRedoAction, createUndoAction } from "../actions/actionHistory";
|
|||
import { ActionManager } from "../actions/manager";
|
||||
import { actions } from "../actions/register";
|
||||
import { ActionResult } from "../actions/types";
|
||||
import {
|
||||
EVENT_DIALOG,
|
||||
EVENT_LIBRARY,
|
||||
EVENT_SHAPE,
|
||||
trackEvent,
|
||||
} from "../analytics";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { getDefaultAppState } from "../appState";
|
||||
import {
|
||||
copyToClipboard,
|
||||
|
@ -111,7 +106,7 @@ import {
|
|||
selectGroupsForSelectedElements,
|
||||
} from "../groups";
|
||||
import { createHistory, SceneHistory } from "../history";
|
||||
import { getLanguage, t } from "../i18n";
|
||||
import { defaultLang, getLanguage, languages, setLanguage, t } from "../i18n";
|
||||
import {
|
||||
CODES,
|
||||
getResizeCenterPointKey,
|
||||
|
@ -163,6 +158,7 @@ import {
|
|||
import ContextMenu from "./ContextMenu";
|
||||
import LayerUI from "./LayerUI";
|
||||
import { Stats } from "./Stats";
|
||||
import { Toast } from "./Toast";
|
||||
|
||||
const { history } = createHistory();
|
||||
|
||||
|
@ -332,7 +328,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||
offsetLeft,
|
||||
} = this.state;
|
||||
|
||||
const { onCollabButtonClick, onExportToBackend } = this.props;
|
||||
const { onCollabButtonClick, onExportToBackend, renderFooter } = this.props;
|
||||
const canvasScale = window.devicePixelRatio;
|
||||
|
||||
const canvasWidth = canvasDOMWidth * canvasScale;
|
||||
|
@ -369,9 +365,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||
}
|
||||
zenModeEnabled={zenModeEnabled}
|
||||
toggleZenMode={this.toggleZenMode}
|
||||
lng={getLanguage().lng}
|
||||
langCode={getLanguage().code}
|
||||
isCollaborating={this.props.isCollaborating || false}
|
||||
onExportToBackend={onExportToBackend}
|
||||
renderCustomFooter={renderFooter}
|
||||
/>
|
||||
{this.state.showStats && (
|
||||
<Stats
|
||||
|
@ -380,6 +377,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||
onClose={this.toggleStats}
|
||||
/>
|
||||
)}
|
||||
{this.state.toastMessage !== null && (
|
||||
<Toast
|
||||
message={this.state.toastMessage}
|
||||
clearToast={this.clearToast}
|
||||
/>
|
||||
)}
|
||||
<main>
|
||||
<canvas
|
||||
id="canvas"
|
||||
|
@ -503,7 +506,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||
)
|
||||
) {
|
||||
await Library.importLibrary(blob);
|
||||
trackEvent(EVENT_LIBRARY, "import");
|
||||
this.setState({
|
||||
isLibraryOpen: true,
|
||||
});
|
||||
|
@ -738,6 +740,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||
}
|
||||
|
||||
componentDidUpdate(prevProps: ExcalidrawProps, prevState: AppState) {
|
||||
if (prevProps.langCode !== this.props.langCode) {
|
||||
this.updateLanguage();
|
||||
}
|
||||
|
||||
if (
|
||||
prevProps.width !== this.props.width ||
|
||||
prevProps.height !== this.props.height ||
|
||||
|
@ -912,6 +918,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||
this.canvas!,
|
||||
this.state,
|
||||
);
|
||||
this.setState({ toastMessage: t("toast.copyToClipboardAsPng") });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.setState({ errorMessage: error.message });
|
||||
|
@ -1129,7 +1136,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||
|
||||
toggleLock = () => {
|
||||
this.setState((prevState) => {
|
||||
trackEvent(EVENT_SHAPE, "lock", !prevState.elementLocked ? "on" : "off");
|
||||
return {
|
||||
elementLocked: !prevState.elementLocked,
|
||||
elementType: prevState.elementLocked
|
||||
|
@ -1153,7 +1159,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||
|
||||
toggleStats = () => {
|
||||
if (!this.state.showStats) {
|
||||
trackEvent(EVENT_DIALOG, "stats");
|
||||
trackEvent("dialog", "stats");
|
||||
}
|
||||
this.setState({
|
||||
showStats: !this.state.showStats,
|
||||
|
@ -1170,6 +1176,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||
});
|
||||
};
|
||||
|
||||
clearToast = () => {
|
||||
this.setState({ toastMessage: null });
|
||||
};
|
||||
|
||||
public updateScene = withBatchedUpdates((sceneData: SceneData) => {
|
||||
if (sceneData.commitToHistory) {
|
||||
history.resumeRecording();
|
||||
|
@ -1265,9 +1275,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||
}
|
||||
|
||||
if (event.code === CODES.NINE) {
|
||||
if (!this.state.isLibraryOpen) {
|
||||
trackEvent(EVENT_DIALOG, "library");
|
||||
}
|
||||
this.setState({ isLibraryOpen: !this.state.isLibraryOpen });
|
||||
}
|
||||
|
||||
|
@ -1352,7 +1359,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||
) {
|
||||
const shape = findShapeByKey(event.key);
|
||||
if (shape) {
|
||||
trackEvent(EVENT_SHAPE, shape, "shortcut");
|
||||
this.selectShapeTool(shape);
|
||||
} else if (event.key === KEYS.Q) {
|
||||
this.toggleLock();
|
||||
|
@ -1736,7 +1742,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||
resetCursor();
|
||||
|
||||
if (!event[KEYS.CTRL_OR_CMD]) {
|
||||
trackEvent(EVENT_SHAPE, "text", "double-click");
|
||||
this.startTextEditing({
|
||||
sceneX,
|
||||
sceneY,
|
||||
|
@ -3136,7 +3141,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||
);
|
||||
}
|
||||
this.setState({ suggestedBindings: [], startBoundElement: null });
|
||||
if (!elementLocked) {
|
||||
if (!elementLocked && elementType !== "draw") {
|
||||
resetCursor();
|
||||
this.setState((prevState) => ({
|
||||
draggingElement: null,
|
||||
|
@ -3283,7 +3288,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!elementLocked && draggingElement) {
|
||||
if (!elementLocked && elementType !== "draw" && draggingElement) {
|
||||
this.setState((prevState) => ({
|
||||
selectedElementIds: {
|
||||
...prevState.selectedElementIds,
|
||||
|
@ -3307,7 +3312,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||
);
|
||||
}
|
||||
|
||||
if (!elementLocked) {
|
||||
if (!elementLocked && elementType !== "draw") {
|
||||
resetCursor();
|
||||
this.setState({
|
||||
draggingElement: null,
|
||||
|
@ -3645,6 +3650,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||
label: t("labels.gridMode"),
|
||||
action: this.toggleGridMode,
|
||||
},
|
||||
{
|
||||
checked: this.state.zenModeEnabled,
|
||||
shortcutName: "zenMode",
|
||||
label: t("buttons.zenMode"),
|
||||
action: this.toggleZenMode,
|
||||
},
|
||||
{
|
||||
checked: this.state.showStats,
|
||||
shortcutName: "stats",
|
||||
|
@ -3817,7 +3828,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||
};
|
||||
|
||||
private resetShouldCacheIgnoreZoomDebounced = debounce(() => {
|
||||
this.setState({ shouldCacheIgnoreZoom: false });
|
||||
if (!this.unmounted) {
|
||||
this.setState({ shouldCacheIgnoreZoom: false });
|
||||
}
|
||||
}, 300);
|
||||
|
||||
private getCanvasOffsets(offsets?: {
|
||||
|
@ -3849,6 +3862,14 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||
offsetTop: typeof offsets?.offsetTop === "number" ? offsets.offsetTop : 0,
|
||||
};
|
||||
}
|
||||
|
||||
private async updateLanguage() {
|
||||
const currentLang =
|
||||
languages.find((lang) => lang.code === this.props.langCode) ||
|
||||
defaultLang;
|
||||
await setLanguage(currentLang);
|
||||
this.setAppState({});
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React from "react";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { EVENT_CHANGE, trackEvent } from "../analytics";
|
||||
import { AppState } from "../types";
|
||||
import { DarkModeToggle } from "./DarkModeToggle";
|
||||
|
||||
|
@ -19,8 +18,6 @@ export const BackgroundPickerAndDarkModeToggle = ({
|
|||
<DarkModeToggle
|
||||
value={appState.appearance}
|
||||
onChange={(appearance) => {
|
||||
// TODO: track the theme on the first load too
|
||||
trackEvent(EVENT_CHANGE, "theme", appearance);
|
||||
setAppState({ appearance });
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -6,7 +6,6 @@ import useIsMobile from "../is-mobile";
|
|||
import { users } from "./icons";
|
||||
|
||||
import "./CollabButton.scss";
|
||||
import { EVENT_DIALOG, trackEvent } from "../analytics";
|
||||
|
||||
const CollabButton = ({
|
||||
isCollaborating,
|
||||
|
@ -23,10 +22,7 @@ const CollabButton = ({
|
|||
className={clsx("CollabButton", {
|
||||
"is-collaborating": isCollaborating,
|
||||
})}
|
||||
onClick={() => {
|
||||
trackEvent(EVENT_DIALOG, "collaboration");
|
||||
onClick();
|
||||
}}
|
||||
onClick={onClick}
|
||||
icon={users}
|
||||
type="button"
|
||||
title={t("buttons.roomDialog")}
|
||||
|
|
|
@ -54,6 +54,7 @@
|
|||
.context-menu-option__shortcut {
|
||||
justify-self: end;
|
||||
opacity: 0.6;
|
||||
font-family: inherit;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,11 +52,11 @@ const ContextMenu = ({ options, onCloseRequest, top, left }: Props) => {
|
|||
onClick={action}
|
||||
>
|
||||
<div className="context-menu-option__label">{label}</div>
|
||||
<div className="context-menu-option__shortcut">
|
||||
<kbd className="context-menu-option__shortcut">
|
||||
{shortcutName
|
||||
? getShortcutFromShortcutName(shortcutName)
|
||||
: ""}
|
||||
</div>
|
||||
</kbd>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
@import "../css/_variables";
|
||||
|
||||
.excalidraw {
|
||||
.Dialog {
|
||||
user-select: text;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.Dialog__title {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { render, unmountComponentAtNode } from "react-dom";
|
||||
import { ActionsManagerInterface } from "../actions/types";
|
||||
import { EVENT_DIALOG, trackEvent } from "../analytics";
|
||||
import { probablySupportsClipboardBlob } from "../clipboard";
|
||||
import { canvasToBlob } from "../data/blob";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
|
@ -251,7 +250,6 @@ export const ExportDialog = ({
|
|||
<>
|
||||
<ToolButton
|
||||
onClick={() => {
|
||||
trackEvent(EVENT_DIALOG, "export");
|
||||
setModalIsShown(true);
|
||||
}}
|
||||
icon={exportFile}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React from "react";
|
||||
import oc from "open-color";
|
||||
import { EVENT_EXIT, trackEvent } from "../analytics";
|
||||
import React from "react";
|
||||
|
||||
// https://github.com/tholman/github-corners
|
||||
export const GitHubCorner = React.memo(
|
||||
|
@ -17,9 +16,6 @@ export const GitHubCorner = React.memo(
|
|||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="GitHub repository"
|
||||
onClick={() => {
|
||||
trackEvent(EVENT_EXIT, "github");
|
||||
}}
|
||||
>
|
||||
<path
|
||||
d="M0 0l115 115h15l12 27 108 108V0z"
|
||||
|
|
|
@ -102,6 +102,7 @@
|
|||
position: absolute;
|
||||
bottom: 2px;
|
||||
font-size: 0.7em;
|
||||
color: var(--keybinding-color);
|
||||
|
||||
:root[dir="ltr"] & {
|
||||
right: 2px;
|
||||
|
|
|
@ -1,18 +1,29 @@
|
|||
import React from "react";
|
||||
|
||||
import { LoadingMessage } from "./LoadingMessage";
|
||||
import { setLanguageFirstTime } from "../i18n";
|
||||
import {
|
||||
defaultLang,
|
||||
Language,
|
||||
languages,
|
||||
setLanguageFirstTime,
|
||||
} from "../i18n";
|
||||
|
||||
export class InitializeApp extends React.Component<
|
||||
any,
|
||||
{ isLoading: boolean }
|
||||
> {
|
||||
interface Props {
|
||||
langCode: Language["code"];
|
||||
}
|
||||
interface State {
|
||||
isLoading: boolean;
|
||||
}
|
||||
export class InitializeApp extends React.Component<Props, State> {
|
||||
public state: { isLoading: boolean } = {
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
await setLanguageFirstTime();
|
||||
const currentLang =
|
||||
languages.find((lang) => lang.code === this.props.langCode) ||
|
||||
defaultLang;
|
||||
await setLanguageFirstTime(currentLang);
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
});
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import * as i18n from "../i18n";
|
||||
|
||||
export const LanguageList = ({
|
||||
onChange,
|
||||
languages = i18n.languages,
|
||||
currentLanguage = i18n.getLanguage().lng,
|
||||
floating,
|
||||
}: {
|
||||
languages?: { lng: string; label: string }[];
|
||||
onChange: (value: string) => void;
|
||||
currentLanguage?: string;
|
||||
floating?: boolean;
|
||||
}) => (
|
||||
<React.Fragment>
|
||||
<select
|
||||
className={clsx("dropdown-select dropdown-select__language", {
|
||||
"dropdown-select--floating": floating,
|
||||
})}
|
||||
onChange={({ target }) => onChange(target.value)}
|
||||
value={currentLanguage}
|
||||
aria-label={i18n.t("buttons.selectLanguage")}
|
||||
>
|
||||
{languages.map((language) => (
|
||||
<option key={language.lng} value={language.lng}>
|
||||
{language.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</React.Fragment>
|
||||
);
|
|
@ -1,57 +1,46 @@
|
|||
import clsx from "clsx";
|
||||
import React, {
|
||||
RefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
RefObject,
|
||||
useEffect,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import { showSelectedShapeActions } from "../element";
|
||||
import { calculateScrollCenter, getSelectedElements } from "../scene";
|
||||
import { exportCanvas } from "../data";
|
||||
|
||||
import { AppState, LibraryItems, LibraryItem } from "../types";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { Island } from "./Island";
|
||||
import Stack from "./Stack";
|
||||
import { FixedSideContainer } from "./FixedSideContainer";
|
||||
import { UserList } from "./UserList";
|
||||
import { LockIcon } from "./LockIcon";
|
||||
import { ExportDialog, ExportCB } from "./ExportDialog";
|
||||
import { LanguageList } from "./LanguageList";
|
||||
import { t, languages, setLanguage } from "../i18n";
|
||||
import { HintViewer } from "./HintViewer";
|
||||
import { CLASSES } from "../constants";
|
||||
import { exportCanvas } from "../data";
|
||||
import { importLibraryFromJSON, saveLibraryAsJSON } from "../data/json";
|
||||
import { Library } from "../data/library";
|
||||
import { showSelectedShapeActions } from "../element";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { Language, t } from "../i18n";
|
||||
import useIsMobile from "../is-mobile";
|
||||
|
||||
import { calculateScrollCenter, getSelectedElements } from "../scene";
|
||||
import { ExportType } from "../scene/types";
|
||||
import { MobileMenu } from "./MobileMenu";
|
||||
import { ZoomActions, SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
||||
import { Section } from "./Section";
|
||||
import { AppState, LibraryItem, LibraryItems } from "../types";
|
||||
import { muteFSAbortError } from "../utils";
|
||||
import { SelectedShapeActions, ShapesSwitcher, ZoomActions } from "./Actions";
|
||||
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
||||
import CollabButton from "./CollabButton";
|
||||
import { ErrorDialog } from "./ErrorDialog";
|
||||
import { ShortcutsDialog } from "./ShortcutsDialog";
|
||||
import { LoadingMessage } from "./LoadingMessage";
|
||||
import { CLASSES } from "../constants";
|
||||
import { shield, exportFile, load } from "./icons";
|
||||
import { ExportCB, ExportDialog } from "./ExportDialog";
|
||||
import { FixedSideContainer } from "./FixedSideContainer";
|
||||
import { GitHubCorner } from "./GitHubCorner";
|
||||
import { Tooltip } from "./Tooltip";
|
||||
|
||||
import { HintViewer } from "./HintViewer";
|
||||
import { exportFile, load, shield } from "./icons";
|
||||
import { Island } from "./Island";
|
||||
import "./LayerUI.scss";
|
||||
import { LibraryUnit } from "./LibraryUnit";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { saveLibraryAsJSON, importLibraryFromJSON } from "../data/json";
|
||||
import { muteFSAbortError } from "../utils";
|
||||
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
||||
import clsx from "clsx";
|
||||
import { Library } from "../data/library";
|
||||
import {
|
||||
EVENT_ACTION,
|
||||
EVENT_EXIT,
|
||||
EVENT_LIBRARY,
|
||||
trackEvent,
|
||||
} from "../analytics";
|
||||
import { LoadingMessage } from "./LoadingMessage";
|
||||
import { LockIcon } from "./LockIcon";
|
||||
import { MobileMenu } from "./MobileMenu";
|
||||
import { PasteChartDialog } from "./PasteChartDialog";
|
||||
import { Section } from "./Section";
|
||||
import { ShortcutsDialog } from "./ShortcutsDialog";
|
||||
import Stack from "./Stack";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { Tooltip } from "./Tooltip";
|
||||
import { UserList } from "./UserList";
|
||||
|
||||
interface LayerUIProps {
|
||||
actionManager: ActionManager;
|
||||
|
@ -64,13 +53,14 @@ interface LayerUIProps {
|
|||
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
||||
zenModeEnabled: boolean;
|
||||
toggleZenMode: () => void;
|
||||
lng: string;
|
||||
langCode: Language["code"];
|
||||
isCollaborating: boolean;
|
||||
onExportToBackend?: (
|
||||
exportedElements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
canvas: HTMLCanvasElement | null,
|
||||
) => void;
|
||||
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
|
||||
}
|
||||
|
||||
const useOnClickOutside = (
|
||||
|
@ -159,13 +149,7 @@ const LibraryMenuItems = ({
|
|||
}}
|
||||
/>
|
||||
|
||||
<a
|
||||
href="https://libraries.excalidraw.com"
|
||||
target="_excalidraw_libraries"
|
||||
onClick={() => {
|
||||
trackEvent(EVENT_EXIT, "libraries");
|
||||
}}
|
||||
>
|
||||
<a href="https://libraries.excalidraw.com" target="_excalidraw_libraries">
|
||||
{t("labels.libraries")}
|
||||
</a>
|
||||
</div>,
|
||||
|
@ -267,7 +251,6 @@ const LibraryMenu = ({
|
|||
const items = await Library.loadLibrary();
|
||||
const nextItems = items.filter((_, index) => index !== indexToRemove);
|
||||
Library.saveLibrary(nextItems);
|
||||
trackEvent(EVENT_LIBRARY, "remove");
|
||||
setLibraryItems(nextItems);
|
||||
}, []);
|
||||
|
||||
|
@ -276,7 +259,6 @@ const LibraryMenu = ({
|
|||
const items = await Library.loadLibrary();
|
||||
const nextItems = [...items, elements];
|
||||
onAddToLibrary();
|
||||
trackEvent(EVENT_LIBRARY, "add");
|
||||
Library.saveLibrary(nextItems);
|
||||
setLibraryItems(nextItems);
|
||||
},
|
||||
|
@ -316,6 +298,7 @@ const LayerUI = ({
|
|||
toggleZenMode,
|
||||
isCollaborating,
|
||||
onExportToBackend,
|
||||
renderCustomFooter,
|
||||
}: LayerUIProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
|
@ -327,9 +310,6 @@ const LayerUI = ({
|
|||
href="https://blog.excalidraw.com/end-to-end-encryption/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={() => {
|
||||
trackEvent(EVENT_EXIT, "e2ee shield");
|
||||
}}
|
||||
>
|
||||
<Tooltip label={t("encrypted.tooltip")} position="above" long={true}>
|
||||
{shield}
|
||||
|
@ -551,14 +531,7 @@ const LayerUI = ({
|
|||
"transition-right disable-pointerEvents": zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
<LanguageList
|
||||
onChange={async (lng) => {
|
||||
await setLanguage(lng);
|
||||
setAppState({});
|
||||
}}
|
||||
languages={languages}
|
||||
floating
|
||||
/>
|
||||
{renderCustomFooter?.(false)}
|
||||
{actionManager.renderAction("toggleShortcuts")}
|
||||
</div>
|
||||
<button
|
||||
|
@ -573,7 +546,6 @@ const LayerUI = ({
|
|||
<button
|
||||
className="scroll-back-to-content"
|
||||
onClick={() => {
|
||||
trackEvent(EVENT_ACTION, "scroll to content");
|
||||
setAppState({
|
||||
...calculateScrollCenter(elements, appState, canvas),
|
||||
});
|
||||
|
@ -628,6 +600,7 @@ const LayerUI = ({
|
|||
onLockToggle={onLockToggle}
|
||||
canvas={canvas}
|
||||
isCollaborating={isCollaborating}
|
||||
renderCustomFooter={renderCustomFooter}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
@ -665,9 +638,8 @@ const areEqual = (prev: LayerUIProps, next: LayerUIProps) => {
|
|||
const nextAppState = getNecessaryObj(next.appState);
|
||||
|
||||
const keys = Object.keys(prevAppState) as (keyof Partial<AppState>)[];
|
||||
|
||||
return (
|
||||
prev.lng === next.lng &&
|
||||
prev.langCode === next.langCode &&
|
||||
prev.elements === next.elements &&
|
||||
keys.every((key) => prevAppState[key] === nextAppState[key])
|
||||
);
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import React from "react";
|
||||
import { AppState } from "../types";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { t, setLanguage } from "../i18n";
|
||||
import { t } from "../i18n";
|
||||
import Stack from "./Stack";
|
||||
import { LanguageList } from "./LanguageList";
|
||||
import { showSelectedShapeActions } from "../element";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { FixedSideContainer } from "./FixedSideContainer";
|
||||
|
@ -17,7 +16,6 @@ import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
|
|||
import { LockIcon } from "./LockIcon";
|
||||
import { UserList } from "./UserList";
|
||||
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
||||
import { EVENT_ACTION, trackEvent } from "../analytics";
|
||||
|
||||
type MobileMenuProps = {
|
||||
appState: AppState;
|
||||
|
@ -30,6 +28,7 @@ type MobileMenuProps = {
|
|||
onLockToggle: () => void;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
isCollaborating: boolean;
|
||||
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
|
||||
};
|
||||
|
||||
export const MobileMenu = ({
|
||||
|
@ -43,6 +42,7 @@ export const MobileMenu = ({
|
|||
onLockToggle,
|
||||
canvas,
|
||||
isCollaborating,
|
||||
renderCustomFooter,
|
||||
}: MobileMenuProps) => (
|
||||
<>
|
||||
<FixedSideContainer side="top">
|
||||
|
@ -102,15 +102,7 @@ export const MobileMenu = ({
|
|||
appState={appState}
|
||||
setAppState={setAppState}
|
||||
/>
|
||||
<fieldset>
|
||||
<legend>{t("labels.language")}</legend>
|
||||
<LanguageList
|
||||
onChange={async (lng) => {
|
||||
await setLanguage(lng);
|
||||
setAppState({});
|
||||
}}
|
||||
/>
|
||||
</fieldset>
|
||||
{renderCustomFooter?.(true)}
|
||||
<fieldset>
|
||||
<legend>{t("labels.collaborators")}</legend>
|
||||
<UserList mobile>
|
||||
|
@ -156,7 +148,6 @@ export const MobileMenu = ({
|
|||
<button
|
||||
className="scroll-back-to-content"
|
||||
onClick={() => {
|
||||
trackEvent(EVENT_ACTION, "scroll to content");
|
||||
setAppState({
|
||||
...calculateScrollCenter(elements, appState, canvas),
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import oc from "open-color";
|
||||
import React, { useLayoutEffect, useRef, useState } from "react";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { ChartElements, renderSpreadsheet, Spreadsheet } from "../charts";
|
||||
import { ChartType } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
|
@ -86,6 +87,7 @@ export const PasteChartDialog = ({
|
|||
|
||||
const handleChartClick = (chartType: ChartType, elements: ChartElements) => {
|
||||
onInsertChart(elements);
|
||||
trackEvent("magic", "chart", chartType);
|
||||
setAppState({
|
||||
currentChartType: chartType,
|
||||
pasteDialog: {
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.ShortcutsDialog-footer {
|
||||
|
|
|
@ -4,7 +4,6 @@ import { isDarwin } from "../keys";
|
|||
import { Dialog } from "./Dialog";
|
||||
import { getShortcutKey } from "../utils";
|
||||
import "./ShortcutsDialog.scss";
|
||||
import { EVENT_EXIT, trackEvent } from "../analytics";
|
||||
|
||||
const Columns = (props: { children: React.ReactNode }) => (
|
||||
<div
|
||||
|
@ -83,7 +82,7 @@ Shortcut.defaultProps = {
|
|||
};
|
||||
|
||||
const ShortcutKey = (props: { children: React.ReactNode }) => (
|
||||
<span className="ShorcutsDialog-key" {...props} />
|
||||
<kbd className="ShorcutsDialog-key" {...props} />
|
||||
);
|
||||
|
||||
const Footer = () => (
|
||||
|
@ -92,29 +91,13 @@ const Footer = () => (
|
|||
href="https://blog.excalidraw.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={() => {
|
||||
trackEvent(EVENT_EXIT, "blog");
|
||||
}}
|
||||
>
|
||||
{t("shortcutsDialog.blog")}
|
||||
</a>
|
||||
<a
|
||||
href="https://howto.excalidraw.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={() => {
|
||||
trackEvent(EVENT_EXIT, "guides");
|
||||
}}
|
||||
>
|
||||
{t("shortcutsDialog.howto")}
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/excalidraw/excalidraw/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={() => {
|
||||
trackEvent(EVENT_EXIT, "issues");
|
||||
}}
|
||||
>
|
||||
{t("shortcutsDialog.github")}
|
||||
</a>
|
||||
|
|
32
src/components/Toast.scss
Normal file
32
src/components/Toast.scss
Normal file
|
@ -0,0 +1,32 @@
|
|||
@import "../css/_variables";
|
||||
|
||||
.excalidraw {
|
||||
.Toast {
|
||||
animation: fade-in 0.5s;
|
||||
background-color: var(--button-gray-1);
|
||||
border-radius: 4px;
|
||||
bottom: 10px;
|
||||
box-sizing: border-box;
|
||||
cursor: default;
|
||||
left: 50%;
|
||||
margin-left: -150px;
|
||||
padding: 4px 0;
|
||||
position: fixed;
|
||||
text-align: center;
|
||||
width: 300px;
|
||||
z-index: 999999;
|
||||
}
|
||||
|
||||
.Toast__message {
|
||||
color: var(--popup-text-color);
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
34
src/components/Toast.tsx
Normal file
34
src/components/Toast.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import { TOAST_TIMEOUT } from "../constants";
|
||||
import "./Toast.scss";
|
||||
|
||||
export const Toast = ({
|
||||
message,
|
||||
clearToast,
|
||||
}: {
|
||||
message: string;
|
||||
clearToast: () => void;
|
||||
}) => {
|
||||
const timerRef = useRef<number>(0);
|
||||
|
||||
const scheduleTimeout = useCallback(
|
||||
() =>
|
||||
(timerRef.current = window.setTimeout(() => clearToast(), TOAST_TIMEOUT)),
|
||||
[clearToast],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
scheduleTimeout();
|
||||
return () => clearTimeout(timerRef.current);
|
||||
}, [scheduleTimeout, message]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="Toast"
|
||||
onMouseEnter={() => clearTimeout(timerRef?.current)}
|
||||
onMouseLeave={scheduleTimeout}
|
||||
>
|
||||
<p className="Toast__message">{message}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue