Merge remote-tracking branch 'origin/master' into danieljgeiger-mathjax

This commit is contained in:
Daniel J. Geiger 2023-01-07 11:58:15 -06:00
commit 45faf7d58f
82 changed files with 750 additions and 624 deletions

View file

@ -25,7 +25,7 @@ export const actionUnbindText = register({
name: "unbindText",
contextItemLabel: "labels.unbindText",
trackEvent: { category: "element" },
contextItemPredicate: (elements, appState) => {
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
return selectedElements.some((element) => hasBoundTextElement(element));
},
@ -75,7 +75,7 @@ export const actionBindText = register({
name: "bindText",
contextItemLabel: "labels.bindText",
trackEvent: { category: "element" },
contextItemPredicate: (elements, appState) => {
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
if (selectedElements.length === 2) {

View file

@ -1,11 +1,5 @@
import { ColorPicker } from "../components/ColorPicker";
import {
eraser,
MoonIcon,
SunIcon,
ZoomInIcon,
ZoomOutIcon,
} from "../components/icons";
import { eraser, ZoomInIcon, ZoomOutIcon } from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { MIN_ZOOM, THEME, ZOOM_STEP } from "../constants";
import { getCommonBounds, getNonDeletedElements } from "../element";
@ -21,14 +15,17 @@ import { register } from "./register";
import { Tooltip } from "../components/Tooltip";
import { newElementWith } from "../element/mutateElement";
import { getDefaultAppState, isEraserActive } from "../appState";
import ClearCanvas from "../components/ClearCanvas";
import clsx from "clsx";
import DropdownMenuItem from "../components/dropdownMenu/DropdownMenuItem";
import { getShortcutFromShortcutName } from "./shortcuts";
export const actionChangeViewBackgroundColor = register({
name: "changeViewBackgroundColor",
trackEvent: false,
predicate: (elements, appState, props, app) => {
return (
!!app.props.UIOptions.canvasActions.changeViewBackgroundColor &&
!appState.viewModeEnabled
);
},
perform: (_, appState, value) => {
return {
appState: { ...appState, ...value },
@ -36,6 +33,7 @@ export const actionChangeViewBackgroundColor = register({
};
},
PanelComponent: ({ elements, appState, updateData }) => {
// FIXME move me to src/components/mainMenu/DefaultItems.tsx
return (
<div style={{ position: "relative" }}>
<ColorPicker
@ -59,6 +57,12 @@ export const actionChangeViewBackgroundColor = register({
export const actionClearCanvas = register({
name: "clearCanvas",
trackEvent: { category: "canvas" },
predicate: (elements, appState, props, app) => {
return (
!!app.props.UIOptions.canvasActions.clearCanvas &&
!appState.viewModeEnabled
);
},
perform: (elements, appState, _, app) => {
app.imageCache.clear();
return {
@ -84,8 +88,6 @@ export const actionClearCanvas = register({
commitToHistory: true,
};
},
PanelComponent: ({ updateData }) => <ClearCanvas onConfirm={updateData} />,
});
export const actionZoomIn = register({
@ -298,26 +300,10 @@ export const actionToggleTheme = register({
commitToHistory: false,
};
},
PanelComponent: ({ appState, updateData }) => (
<DropdownMenuItem
onSelect={() => {
updateData(appState.theme === THEME.LIGHT ? THEME.DARK : THEME.LIGHT);
}}
icon={appState.theme === "dark" ? SunIcon : MoonIcon}
dataTestId="toggle-dark-mode"
shortcut={getShortcutFromShortcutName("toggleTheme")}
ariaLabel={
appState.theme === "dark"
? t("buttons.lightMode")
: t("buttons.darkMode")
}
>
{appState.theme === "dark"
? t("buttons.lightMode")
: t("buttons.darkMode")}
</DropdownMenuItem>
),
keyTest: (event) => event.altKey && event.shiftKey && event.code === CODES.D,
predicate: (elements, appState, props, app) => {
return !!app.props.UIOptions.canvasActions.toggleTheme;
},
});
export const actionErase = register({

View file

@ -24,7 +24,7 @@ export const actionCopy = register({
commitToHistory: false,
};
},
contextItemPredicate: (elements, appState, appProps, app) => {
predicate: (elements, appState, appProps, app) => {
return app.device.isMobile && !!navigator.clipboard;
},
contextItemLabel: "labels.copy",
@ -41,7 +41,7 @@ export const actionPaste = register({
commitToHistory: false,
};
},
contextItemPredicate: (elements, appState, appProps, app) => {
predicate: (elements, appState, appProps, app) => {
return app.device.isMobile && !!navigator.clipboard;
},
contextItemLabel: "labels.paste",
@ -56,7 +56,7 @@ export const actionCut = register({
actionCopy.perform(elements, appState, data, app);
return actionDeleteSelected.perform(elements, appState);
},
contextItemPredicate: (elements, appState, appProps, app) => {
predicate: (elements, appState, appProps, app) => {
return app.device.isMobile && !!navigator.clipboard;
},
contextItemLabel: "labels.cut",
@ -101,7 +101,7 @@ export const actionCopyAsSvg = register({
};
}
},
contextItemPredicate: (elements) => {
predicate: (elements) => {
return probablySupportsClipboardWriteText && elements.length > 0;
},
contextItemLabel: "labels.copyAsSvg",
@ -158,7 +158,7 @@ export const actionCopyAsPng = register({
};
}
},
contextItemPredicate: (elements) => {
predicate: (elements) => {
return probablySupportsClipboardBlob && elements.length > 0;
},
contextItemLabel: "labels.copyAsPng",
@ -188,7 +188,7 @@ export const copyText = register({
commitToHistory: false,
};
},
contextItemPredicate: (elements, appState) => {
predicate: (elements, appState) => {
return (
probablySupportsClipboardWriteText &&
getSelectedElements(elements, appState, true).some(isTextElement)

View file

@ -1,7 +1,6 @@
import { LoadIcon, questionCircle, saveAs } from "../components/icons";
import { questionCircle, saveAs } from "../components/icons";
import { ProjectName } from "../components/ProjectName";
import { ToolButton } from "../components/ToolButton";
import "../components/ToolIcon.scss";
import { Tooltip } from "../components/Tooltip";
import { DarkModeToggle } from "../components/DarkModeToggle";
import { loadFromJSON, saveAsJSON } from "../data";
@ -15,12 +14,11 @@ import { getExportSize } from "../scene/export";
import { DEFAULT_EXPORT_PADDING, EXPORT_SCALES, THEME } from "../constants";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { getNonDeletedElements } from "../element";
import { ActiveFile } from "../components/ActiveFile";
import { isImageFileHandle } from "../data/blob";
import { nativeFileSystemSupported } from "../data/filesystem";
import { Theme } from "../element/types";
import DropdownMenuItem from "../components/dropdownMenu/DropdownMenuItem";
import { getShortcutFromShortcutName } from "./shortcuts";
import "../components/ToolIcon.scss";
export const actionChangeProjectName = register({
name: "changeProjectName",
@ -133,6 +131,13 @@ export const actionChangeExportEmbedScene = register({
export const actionSaveToActiveFile = register({
name: "saveToActiveFile",
trackEvent: { category: "export" },
predicate: (elements, appState, props, app) => {
return (
!!app.props.UIOptions.canvasActions.saveToActiveFile &&
!!appState.fileHandle &&
!appState.viewModeEnabled
);
},
perform: async (elements, appState, value, app) => {
const fileHandleExists = !!appState.fileHandle;
@ -169,12 +174,6 @@ export const actionSaveToActiveFile = register({
},
keyTest: (event) =>
event.key === KEYS.S && event[KEYS.CTRL_OR_CMD] && !event.shiftKey,
PanelComponent: ({ updateData, appState }) => (
<ActiveFile
onSave={() => updateData(null)}
fileName={appState.fileHandle?.name}
/>
),
});
export const actionSaveFileToDisk = register({
@ -220,6 +219,11 @@ export const actionSaveFileToDisk = register({
export const actionLoadScene = register({
name: "loadScene",
trackEvent: { category: "export" },
predicate: (elements, appState, props, app) => {
return (
!!app.props.UIOptions.canvasActions.loadScene && !appState.viewModeEnabled
);
},
perform: async (elements, appState, _, app) => {
try {
const {
@ -247,19 +251,6 @@ export const actionLoadScene = register({
}
},
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O,
PanelComponent: ({ updateData }) => {
return (
<DropdownMenuItem
icon={LoadIcon}
onSelect={updateData}
dataTestId="load-button"
shortcut={getShortcutFromShortcutName("loadScene")}
ariaLabel={t("buttons.load")}
>
{t("buttons.load")}
</DropdownMenuItem>
);
},
});
export const actionExportWithDarkMode = register({

View file

@ -50,7 +50,7 @@ export const actionFlipHorizontal = register({
},
keyTest: (event) => event.shiftKey && event.code === "KeyH",
contextItemLabel: "labels.flipHorizontal",
contextItemPredicate: (elements, appState) =>
predicate: (elements, appState) =>
enableActionFlipHorizontal(elements, appState),
});
@ -67,7 +67,7 @@ export const actionFlipVertical = register({
keyTest: (event) =>
event.shiftKey && event.code === "KeyV" && !event[KEYS.CTRL_OR_CMD],
contextItemLabel: "labels.flipVertical",
contextItemPredicate: (elements, appState) =>
predicate: (elements, appState) =>
enableActionFlipVertical(elements, appState),
});

View file

@ -129,8 +129,7 @@ export const actionGroup = register({
};
},
contextItemLabel: "labels.group",
contextItemPredicate: (elements, appState) =>
enableActionGroup(elements, appState),
predicate: (elements, appState) => enableActionGroup(elements, appState),
keyTest: (event) =>
!event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G,
PanelComponent: ({ elements, appState, updateData }) => (
@ -193,8 +192,7 @@ export const actionUngroup = register({
event[KEYS.CTRL_OR_CMD] &&
event.key === KEYS.G.toUpperCase(),
contextItemLabel: "labels.ungroup",
contextItemPredicate: (elements, appState) =>
getSelectedGroupIds(appState).length > 0,
predicate: (elements, appState) => getSelectedGroupIds(appState).length > 0,
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton

View file

@ -10,7 +10,7 @@ export const actionToggleLinearEditor = register({
trackEvent: {
category: "element",
},
contextItemPredicate: (elements, appState) => {
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
return true;

View file

@ -1,12 +1,10 @@
import { HamburgerMenuIcon, HelpIcon, palette } from "../components/icons";
import { HamburgerMenuIcon, palette } from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
import { showSelectedShapeActions, getNonDeletedElements } from "../element";
import { register } from "./register";
import { allowFullScreen, exitFullScreen, isFullScreen } from "../utils";
import { KEYS } from "../keys";
import { HelpButton } from "../components/HelpButton";
import DropdownMenuItem from "../components/dropdownMenu/DropdownMenuItem";
export const actionToggleCanvasMenu = register({
name: "toggleCanvasMenu",
@ -88,19 +86,5 @@ export const actionShortcuts = register({
commitToHistory: false,
};
},
PanelComponent: ({ updateData, isInHamburgerMenu }) =>
isInHamburgerMenu ? (
<DropdownMenuItem
dataTestId="help-menu-item"
icon={HelpIcon}
onSelect={updateData}
shortcut="?"
ariaLabel={t("helpDialog.title")}
>
{t("helpDialog.title")}
</DropdownMenuItem>
) : (
<HelpButton title={t("helpDialog.title")} onClick={updateData} />
),
keyTest: (event) => event.key === KEYS.QUESTION_MARK,
});

View file

@ -20,7 +20,7 @@ export const actionToggleGridMode = register({
};
},
checked: (appState: AppState) => appState.gridSize !== null,
contextItemPredicate: (element, appState, props) => {
predicate: (element, appState, props) => {
return typeof props.gridModeEnabled === "undefined";
},
contextItemLabel: "labels.showGrid",

View file

@ -18,7 +18,7 @@ export const actionToggleViewMode = register({
};
},
checked: (appState) => appState.viewModeEnabled,
contextItemPredicate: (elements, appState, appProps) => {
predicate: (elements, appState, appProps) => {
return typeof appProps.viewModeEnabled === "undefined";
},
contextItemLabel: "labels.viewMode",

View file

@ -18,7 +18,7 @@ export const actionToggleZenMode = register({
};
},
checked: (appState) => appState.zenModeEnabled,
contextItemPredicate: (elements, appState, appProps) => {
predicate: (elements, appState, appProps) => {
return typeof appProps.zenModeEnabled === "undefined";
},
contextItemLabel: "buttons.zenMode",

View file

@ -108,25 +108,6 @@ export class ActionManager {
}
}
public isActionEnabled(
elements: readonly ExcalidrawElement[],
appState: AppState,
actionName: Action["name"],
): boolean {
if (isActionName(actionName)) {
return !(
actionName in this.disablers &&
this.disablers[actionName].some((fn) =>
fn(elements, appState, actionName),
)
);
}
return (
actionName in this.enablers &&
this.enablers[actionName].some((fn) => fn(elements, appState, actionName))
);
}
registerAction(action: Action) {
this.actions[action.name] = action;
}
@ -143,11 +124,7 @@ export class ActionManager {
(action) =>
(action.name in canvasActions
? canvasActions[action.name as keyof typeof canvasActions]
: this.isActionEnabled(
this.getElementsIncludingDeleted(),
this.getAppState(),
action.name,
)) &&
: this.isActionEnabled(action, { guardsOnly: true })) &&
action.keyTest &&
action.keyTest(
event,
@ -197,7 +174,6 @@ export class ActionManager {
renderAction = (
name: ActionName | Action["name"],
data?: PanelComponentProps["data"],
isInHamburgerMenu = false,
) => {
const canvasActions = this.app.props.UIOptions.canvasActions;
@ -206,11 +182,7 @@ export class ActionManager {
"PanelComponent" in this.actions[name] &&
(name in canvasActions
? canvasActions[name as keyof typeof canvasActions]
: this.isActionEnabled(
this.getElementsIncludingDeleted(),
this.getAppState(),
name,
))
: this.isActionEnabled(this.actions[name], { guardsOnly: true }))
) {
const action = this.actions[name];
const PanelComponent = action.PanelComponent!;
@ -238,11 +210,48 @@ export class ActionManager {
updateData={updateData}
appProps={this.app.props}
data={data}
isInHamburgerMenu={isInHamburgerMenu}
/>
);
}
return null;
};
isActionEnabled = (
action: Action | ActionName,
opts?: {
elements?: readonly ExcalidrawElement[];
data?: Record<string, any>;
guardsOnly?: boolean;
},
): boolean => {
const elements = opts?.elements ?? this.getElementsIncludingDeleted();
const appState = this.getAppState();
const data = opts?.data;
const _action = isActionName(action) ? this.actions[action] : action;
if (
!opts?.guardsOnly &&
_action.predicate &&
!_action.predicate(elements, appState, this.app.props, this.app, data)
) {
return false;
}
if (isActionName(_action.name)) {
return !(
_action.name in this.disablers &&
this.disablers[_action.name].some((fn) =>
fn(elements, appState, _action.name as ActionName),
)
);
}
return (
_action.name in this.enablers &&
this.enablers[_action.name].some((fn) =>
fn(elements, appState, _action.name),
)
);
};
}

View file

@ -146,9 +146,7 @@ export type PanelComponentProps = {
export interface Action {
name: string;
PanelComponent?: React.FC<
PanelComponentProps & { isInHamburgerMenu: boolean }
>;
PanelComponent?: React.FC<PanelComponentProps>;
panelComponentPredicate?: (
elements: readonly ExcalidrawElement[],
appState: AppState,
@ -171,11 +169,12 @@ export interface Action {
elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>,
) => string);
contextItemPredicate?: (
predicate?: (
elements: readonly ExcalidrawElement[],
appState: AppState,
appProps: ExcalidrawProps,
app: AppClassProperties,
data?: Record<string, any>,
) => boolean;
checked?: (appState: Readonly<AppState>) => boolean;
trackEvent: