Refactor: Drop isActionName and convert getCustomActions to

`filterActions`.
This commit is contained in:
Daniel J. Geiger 2023-01-28 21:27:25 -06:00
parent 87aba3f619
commit 14c6ea938a
8 changed files with 145 additions and 133 deletions

View File

@ -6,7 +6,6 @@ import {
PanelComponentProps, PanelComponentProps,
ActionSource, ActionSource,
ActionPredicateFn, ActionPredicateFn,
isActionName,
} from "./types"; } from "./types";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { AppClassProperties, AppState } from "../types"; import { AppClassProperties, AppState } from "../types";
@ -76,29 +75,29 @@ export class ActionManager {
} }
} }
getCustomActions(opts?: { filterActions(
elements?: readonly ExcalidrawElement[]; filter: ActionPredicateFn,
data?: Record<string, any>; opts?: {
guardsOnly?: boolean; elements?: readonly ExcalidrawElement[];
}): Action[] { data?: Record<string, any>;
},
): Action[] {
// For testing // For testing
if (this === undefined) { if (this === undefined) {
return []; return [];
} }
const filter = const elements = opts?.elements ?? this.getElementsIncludingDeleted();
opts !== undefined && const appState = this.getAppState();
("elements" in opts || "data" in opts || "guardsOnly" in opts); const data = opts?.data;
const customActions: Action[] = [];
const actions: Action[] = [];
for (const key in this.actions) { for (const key in this.actions) {
const action = this.actions[key]; const action = this.actions[key];
if ( if (filter(action, elements, appState, data)) {
!isActionName(action.name) && actions.push(action);
(!filter || this.isActionEnabled(action, opts))
) {
customActions.push(action);
} }
} }
return customActions; return actions;
} }
registerAction(action: Action) { registerAction(action: Action) {
@ -117,7 +116,7 @@ export class ActionManager {
(action) => (action) =>
(action.name in canvasActions (action.name in canvasActions
? canvasActions[action.name as keyof typeof canvasActions] ? canvasActions[action.name as keyof typeof canvasActions]
: this.isActionEnabled(action, { guardsOnly: true })) && : this.isActionEnabled(action, { noPredicates: true })) &&
action.keyTest && action.keyTest &&
action.keyTest( action.keyTest(
event, event,
@ -172,7 +171,7 @@ export class ActionManager {
"PanelComponent" in this.actions[name] && "PanelComponent" in this.actions[name] &&
(name in canvasActions (name in canvasActions
? canvasActions[name as keyof typeof canvasActions] ? canvasActions[name as keyof typeof canvasActions]
: this.isActionEnabled(this.actions[name], { guardsOnly: true })) : this.isActionEnabled(this.actions[name], { noPredicates: true }))
) { ) {
const action = this.actions[name]; const action = this.actions[name];
const PanelComponent = action.PanelComponent!; const PanelComponent = action.PanelComponent!;
@ -212,7 +211,7 @@ export class ActionManager {
opts?: { opts?: {
elements?: readonly ExcalidrawElement[]; elements?: readonly ExcalidrawElement[];
data?: Record<string, any>; data?: Record<string, any>;
guardsOnly?: boolean; noPredicates?: boolean;
}, },
): boolean => { ): boolean => {
const elements = opts?.elements ?? this.getElementsIncludingDeleted(); const elements = opts?.elements ?? this.getElementsIncludingDeleted();
@ -220,7 +219,7 @@ export class ActionManager {
const data = opts?.data; const data = opts?.data;
if ( if (
!opts?.guardsOnly && !opts?.noPredicates &&
action.predicate && action.predicate &&
!action.predicate(elements, appState, this.app.props, this.app, data) !action.predicate(elements, appState, this.app.props, this.app, data)
) { ) {

View File

@ -43,92 +43,86 @@ export type ActionPredicateFn = (
export type UpdaterFn = (res: ActionResult) => void; export type UpdaterFn = (res: ActionResult) => void;
export type ActionFilterFn = (action: Action) => void; export type ActionFilterFn = (action: Action) => void;
const actionNames = [ export type ActionName =
"copy", | "copy"
"cut", | "cut"
"paste", | "paste"
"copyAsPng", | "copyAsPng"
"copyAsSvg", | "copyAsSvg"
"copyText", | "copyText"
"sendBackward", | "sendBackward"
"bringForward", | "bringForward"
"sendToBack", | "sendToBack"
"bringToFront", | "bringToFront"
"copyStyles", | "copyStyles"
"selectAll", | "selectAll"
"pasteStyles", | "pasteStyles"
"gridMode", | "gridMode"
"zenMode", | "zenMode"
"stats", | "stats"
"changeStrokeColor", | "changeStrokeColor"
"changeBackgroundColor", | "changeBackgroundColor"
"changeFillStyle", | "changeFillStyle"
"changeStrokeWidth", | "changeStrokeWidth"
"changeStrokeShape", | "changeStrokeShape"
"changeSloppiness", | "changeSloppiness"
"changeStrokeStyle", | "changeStrokeStyle"
"changeArrowhead", | "changeArrowhead"
"changeOpacity", | "changeOpacity"
"changeFontSize", | "changeFontSize"
"toggleCanvasMenu", | "toggleCanvasMenu"
"toggleEditMenu", | "toggleEditMenu"
"undo", | "undo"
"redo", | "redo"
"finalize", | "finalize"
"changeProjectName", | "changeProjectName"
"changeExportBackground", | "changeExportBackground"
"changeExportEmbedScene", | "changeExportEmbedScene"
"changeExportScale", | "changeExportScale"
"saveToActiveFile", | "saveToActiveFile"
"saveFileToDisk", | "saveFileToDisk"
"loadScene", | "loadScene"
"duplicateSelection", | "duplicateSelection"
"deleteSelectedElements", | "deleteSelectedElements"
"changeViewBackgroundColor", | "changeViewBackgroundColor"
"clearCanvas", | "clearCanvas"
"zoomIn", | "zoomIn"
"zoomOut", | "zoomOut"
"resetZoom", | "resetZoom"
"zoomToFit", | "zoomToFit"
"zoomToSelection", | "zoomToSelection"
"changeFontFamily", | "changeFontFamily"
"changeTextAlign", | "changeTextAlign"
"changeVerticalAlign", | "changeVerticalAlign"
"toggleFullScreen", | "toggleFullScreen"
"toggleShortcuts", | "toggleShortcuts"
"group", | "group"
"ungroup", | "ungroup"
"goToCollaborator", | "goToCollaborator"
"addToLibrary", | "addToLibrary"
"changeRoundness", | "changeRoundness"
"alignTop", | "alignTop"
"alignBottom", | "alignBottom"
"alignLeft", | "alignLeft"
"alignRight", | "alignRight"
"alignVerticallyCentered", | "alignVerticallyCentered"
"alignHorizontallyCentered", | "alignHorizontallyCentered"
"distributeHorizontally", | "distributeHorizontally"
"distributeVertically", | "distributeVertically"
"flipHorizontal", | "flipHorizontal"
"flipVertical", | "flipVertical"
"viewMode", | "viewMode"
"exportWithDarkMode", | "exportWithDarkMode"
"toggleTheme", | "toggleTheme"
"increaseFontSize", | "increaseFontSize"
"decreaseFontSize", | "decreaseFontSize"
"unbindText", | "unbindText"
"hyperlink", | "hyperlink"
"bindText", | "bindText"
"toggleLock", | "toggleLock"
"toggleLinearEditor", | "toggleLinearEditor"
"toggleEraserTool", | "toggleEraserTool"
"toggleHandTool", | "toggleHandTool";
] as const;
// So we can have the `isActionName` type guard
export type ActionName = typeof actionNames[number];
export const isActionName = (n: any): n is ActionName =>
actionNames.includes(n);
export type PanelComponentProps = { export type PanelComponentProps = {
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];

View File

@ -3,7 +3,7 @@ import { ActionManager } from "../actions/manager";
import { getNonDeletedElements } from "../element"; import { getNonDeletedElements } from "../element";
import { ExcalidrawElement, PointerType } from "../element/types"; import { ExcalidrawElement, PointerType } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { useDevice, useExcalidrawActionManager } from "../components/App"; import { useDevice } from "../components/App";
import { import {
canChangeRoundness, canChangeRoundness,
canHaveArrowheads, canHaveArrowheads,
@ -23,7 +23,7 @@ import {
} from "../utils"; } from "../utils";
import Stack from "./Stack"; import Stack from "./Stack";
import { ToolButton } from "./ToolButton"; import { ToolButton } from "./ToolButton";
import { SubtypeToggles } from "./SubtypeButton"; import { SubtypeShapeActions, SubtypeToggles } from "./Subtypes";
import { hasStrokeColor } from "../scene/comparisons"; import { hasStrokeColor } from "../scene/comparisons";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { hasBoundTextElement } from "../element/typeChecks"; import { hasBoundTextElement } from "../element/typeChecks";
@ -93,9 +93,7 @@ export const SelectedShapeActions = ({
{showChangeBackgroundIcons && ( {showChangeBackgroundIcons && (
<div>{renderAction("changeBackgroundColor")}</div> <div>{renderAction("changeBackgroundColor")}</div>
)} )}
{useExcalidrawActionManager() <SubtypeShapeActions elements={targetElements} />
.getCustomActions({ elements: targetElements })
.map((action) => renderAction(action.name))}
{showFillIcons && renderAction("changeFillStyle")} {showFillIcons && renderAction("changeFillStyle")}
{(hasStrokeWidth(appState.activeTool.type) || {(hasStrokeWidth(appState.activeTool.type) ||

View File

@ -242,6 +242,7 @@ import {
prepareSubtype, prepareSubtype,
selectSubtype, selectSubtype,
subtypeActionPredicate, subtypeActionPredicate,
isSubtypeAction,
} from "../subtypes"; } from "../subtypes";
import { import {
dataURLToFile, dataURLToFile,
@ -6229,26 +6230,26 @@ class App extends React.Component<AppProps, AppState> {
}; };
private getContextMenuItems = ( private getContextMenuItems = (
type: "canvas" | "element" | "custom", type: "canvas" | "element",
source?: string,
): ContextMenuItems => { ): ContextMenuItems => {
const custom: ContextMenuItems = []; const subtype: ContextMenuItems = [];
this.actionManager this.actionManager
.getCustomActions({ data: { source: source ?? "" } }) .filterActions(isSubtypeAction)
.forEach((action) => custom.push(action)); .forEach(
if (type === "custom") { (action) =>
return custom; this.actionManager.isActionEnabled(action, { data: {} }) &&
} subtype.push(action),
if (custom.length > 0) { );
custom.push(CONTEXT_MENU_SEPARATOR); if (subtype.length > 0) {
subtype.push(CONTEXT_MENU_SEPARATOR);
} }
const standard: ContextMenuItems = this._getContextMenuItems(type).filter( const standard: ContextMenuItems = this._getContextMenuItems(type).filter(
(item) => (item) =>
!item || !item ||
item === CONTEXT_MENU_SEPARATOR || item === CONTEXT_MENU_SEPARATOR ||
this.actionManager.isActionEnabled(item, { guardsOnly: true }), this.actionManager.isActionEnabled(item, { noPredicates: true }),
); );
return [...custom, ...standard]; return [...subtype, ...standard];
}; };
private _getContextMenuItems = ( private _getContextMenuItems = (

View File

@ -7,6 +7,7 @@ import {
Subtype, Subtype,
getSubtypeNames, getSubtypeNames,
hasAlwaysEnabledActions, hasAlwaysEnabledActions,
isSubtypeAction,
isValidSubtype, isValidSubtype,
subtypeCollides, subtypeCollides,
} from "../subtypes"; } from "../subtypes";
@ -30,7 +31,7 @@ export const SubtypeButton = (
const subtypeAction: Action = { const subtypeAction: Action = {
name: subtype, name: subtype,
trackEvent: false, trackEvent: false,
predicate: (...rest) => rest[4]?.source === subtype, predicate: (...rest) => rest[4]?.subtype === subtype,
perform: (elements, appState) => { perform: (elements, appState) => {
const inactive = !appState.activeSubtypes?.includes(subtype) ?? true; const inactive = !appState.activeSubtypes?.includes(subtype) ?? true;
const activeSubtypes: Subtype[] = []; const activeSubtypes: Subtype[] = [];
@ -121,7 +122,7 @@ export const SubtypeToggles = () => {
const onContextMenu = ( const onContextMenu = (
event: React.MouseEvent<HTMLButtonElement>, event: React.MouseEvent<HTMLButtonElement>,
source: string, subtype: string,
) => { ) => {
event.preventDefault(); event.preventDefault();
@ -131,8 +132,9 @@ export const SubtypeToggles = () => {
const top = event.clientY - offsetTop; const top = event.clientY - offsetTop;
const items: ContextMenuItems = []; const items: ContextMenuItems = [];
am.getCustomActions({ data: { source } }).forEach((action) => am.filterActions(isSubtypeAction).forEach(
items.push(action), (action) =>
am.isActionEnabled(action, { data: { subtype } }) && items.push(action),
); );
setAppState({}, () => { setAppState({}, () => {
setAppState({ setAppState({
@ -154,3 +156,18 @@ export const SubtypeToggles = () => {
}; };
SubtypeToggles.displayName = "SubtypeToggles"; SubtypeToggles.displayName = "SubtypeToggles";
export const SubtypeShapeActions = (props: {
elements: readonly ExcalidrawElement[];
}) => {
const am = useExcalidrawActionManager();
return (
<>
{am
.filterActions(isSubtypeAction, { elements: props.elements })
.map((action) => am.renderAction(action.name))}
</>
);
};
SubtypeShapeActions.displayName = "SubtypeShapeActions";

View File

@ -45,7 +45,7 @@ import {
} from "../../../../subtypes"; } from "../../../../subtypes";
import { mathSubtypeIcon } from "./icon"; import { mathSubtypeIcon } from "./icon";
import { getMathSubtypeRecord } from "./types"; import { getMathSubtypeRecord } from "./types";
import { SubtypeButton } from "../../../../components/SubtypeButton"; import { SubtypeButton } from "../../../../components/Subtypes";
import { getMaxContainerWidth } from "../../../../element/newElement"; import { getMaxContainerWidth } from "../../../../element/newElement";
const mathSubtype = getMathSubtypeRecord().subtype; const mathSubtype = getMathSubtypeRecord().subtype;
@ -1318,7 +1318,7 @@ const createMathActions = () => {
getMathProps.getUseTex(appState) getMathProps.getUseTex(appState)
? "labels.useTexTrueActive" ? "labels.useTexTrueActive"
: "labels.useTexTrueInactive", : "labels.useTexTrueInactive",
predicate: (...rest) => rest.length < 5 || rest[4]?.source === mathSubtype, predicate: (...rest) => rest.length < 5 || rest[4]?.subtype === mathSubtype,
trackEvent: false, trackEvent: false,
}; };
const actionUseTexFalse: Action = { const actionUseTexFalse: Action = {
@ -1337,7 +1337,7 @@ const createMathActions = () => {
!getMathProps.getUseTex(appState) !getMathProps.getUseTex(appState)
? "labels.useTexFalseActive" ? "labels.useTexFalseActive"
: "labels.useTexFalseInactive", : "labels.useTexFalseInactive",
predicate: (...rest) => rest.length < 5 || rest[4]?.source === mathSubtype, predicate: (...rest) => rest.length < 5 || rest[4]?.subtype === mathSubtype,
trackEvent: false, trackEvent: false,
}; };
const actionResetUseTex: Action = { const actionResetUseTex: Action = {
@ -1466,8 +1466,7 @@ const createMathActions = () => {
</fieldset> </fieldset>
), ),
predicate: (...rest) => predicate: (...rest) =>
rest[4]?.source === undefined && rest[4] === undefined && enableActionChangeMathProps(rest[0], rest[1]),
enableActionChangeMathProps(rest[0], rest[1]),
trackEvent: false, trackEvent: false,
}; };
const actionMath = SubtypeButton(mathSubtype, "text", mathSubtypeIcon, "M"); const actionMath = SubtypeButton(mathSubtype, "text", mathSubtypeIcon, "M");

View File

@ -99,6 +99,10 @@ const isForSubtype = (
return false; return false;
}; };
export const isSubtypeAction: ActionPredicateFn = function (action) {
return isSubtypeActionName(action.name) && !isSubtypeName(action.name);
};
export const subtypeActionPredicate: ActionPredicateFn = function ( export const subtypeActionPredicate: ActionPredicateFn = function (
action, action,
elements, elements,

View File

@ -18,7 +18,7 @@ import ExcalidrawApp from "../excalidraw-app";
import { ExcalidrawElement, FontString, Theme } from "../element/types"; import { ExcalidrawElement, FontString, Theme } from "../element/types";
import { createIcon, iconFillColor } from "../components/icons"; import { createIcon, iconFillColor } from "../components/icons";
import { SubtypeButton } from "../components/SubtypeButton"; import { SubtypeButton } from "../components/Subtypes";
import { registerAuxLangData } from "../i18n"; import { registerAuxLangData } from "../i18n";
import { getFontString, getShortcutKey } from "../utils"; import { getFontString, getShortcutKey } from "../utils";
import * as textElementUtils from "../element/textElement"; import * as textElementUtils from "../element/textElement";