Browse Source

Filter context menu items through `isActionEnabled`.

Daniel J. Geiger 2 years ago
parent
commit
a5bd54b86d

+ 28 - 17
src/actions/manager.tsx

@@ -75,7 +75,7 @@ export class ActionManager {
     this.app = app;
   }
 
-  public registerActionGuards() {
+  registerActionGuards() {
     const disablers = getActionDisablers();
     for (const d in disablers) {
       const dName = d as ActionName;
@@ -90,7 +90,7 @@ export class ActionManager {
     }
   }
 
-  public registerDisableFn(name: ActionName, disabler: DisableFn) {
+  registerDisableFn(name: ActionName, disabler: DisableFn) {
     if (!(name in this.disablers)) {
       this.disablers[name] = [] as DisableFn[];
     }
@@ -99,7 +99,7 @@ export class ActionManager {
     }
   }
 
-  public registerEnableFn(name: Action["name"], enabler: EnableFn) {
+  registerEnableFn(name: Action["name"], enabler: EnableFn) {
     if (!(name in this.enablers)) {
       this.enablers[name] = [] as EnableFn[];
     }
@@ -108,18 +108,25 @@ export class ActionManager {
     }
   }
 
-  public getCustomActions(opts?: {
+  getCustomActions(opts?: {
     elements?: readonly ExcalidrawElement[];
     data?: Record<string, any>;
+    guardsOnly?: boolean;
   }): Action[] {
     // For testing
     if (this === undefined) {
       return [];
     }
+    const filter =
+      opts !== undefined &&
+      ("elements" in opts || "data" in opts || "guardsOnly" in opts);
     const customActions: Action[] = [];
     for (const key in this.actions) {
       const action = this.actions[key];
-      if (!isActionName(action.name) && this.isActionEnabled(action, opts)) {
+      if (
+        !isActionName(action.name) &&
+        (!filter || this.isActionEnabled(action, opts))
+      ) {
         customActions.push(action);
       }
     }
@@ -142,7 +149,7 @@ export class ActionManager {
         (action) =>
           (action.name in canvasActions
             ? canvasActions[action.name as keyof typeof canvasActions]
-            : this.isActionEnabled(action)) &&
+            : this.isActionEnabled(action, { guardsOnly: true })) &&
           action.keyTest &&
           action.keyTest(
             event,
@@ -200,7 +207,7 @@ export class ActionManager {
       "PanelComponent" in this.actions[name] &&
       (name in canvasActions
         ? canvasActions[name as keyof typeof canvasActions]
-        : this.isActionEnabled(this.actions[name]))
+        : this.isActionEnabled(this.actions[name], { guardsOnly: true }))
     ) {
       const action = this.actions[name];
       const PanelComponent = action.PanelComponent!;
@@ -236,35 +243,39 @@ export class ActionManager {
   };
 
   isActionEnabled = (
-    action: Action,
+    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 (
-      action.predicate &&
-      !action.predicate(elements, appState, this.app.props, this.app, data)
+      !opts?.guardsOnly &&
+      _action.predicate &&
+      !_action.predicate(elements, appState, this.app.props, this.app, data)
     ) {
       return false;
     }
 
-    if (isActionName(action.name)) {
+    if (isActionName(_action.name)) {
       return !(
-        action.name in this.disablers &&
-        this.disablers[action.name].some((fn) =>
-          fn(elements, appState, action.name as ActionName),
+        _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),
+      _action.name in this.enablers &&
+      this.enablers[_action.name].some((fn) =>
+        fn(elements, appState, _action.name),
       )
     );
   };

+ 4 - 6
src/components/Actions.tsx

@@ -3,7 +3,7 @@ import { ActionManager } from "../actions/manager";
 import { getNonDeletedElements } from "../element";
 import { ExcalidrawElement, PointerType } from "../element/types";
 import { t } from "../i18n";
-import { useDevice } from "../components/App";
+import { useDevice, useExcalidrawActionManager } from "../components/App";
 import {
   canChangeRoundness,
   canHaveArrowheads,
@@ -36,12 +36,10 @@ export const SelectedShapeActions = ({
   appState,
   elements,
   renderAction,
-  getCustomActions,
 }: {
   appState: AppState;
   elements: readonly ExcalidrawElement[];
   renderAction: ActionManager["renderAction"];
-  getCustomActions: ActionManager["getCustomActions"];
 }) => {
   const targetElements = getTargetElements(
     getNonDeletedElements(elements),
@@ -94,9 +92,9 @@ export const SelectedShapeActions = ({
       {showChangeBackgroundIcons && (
         <div>{renderAction("changeBackgroundColor")}</div>
       )}
-      {getCustomActions({ elements: targetElements }).map((action) =>
-        renderAction(action.name),
-      )}
+      {useExcalidrawActionManager()
+        .getCustomActions({ elements: targetElements })
+        .map((action) => renderAction(action.name))}
       {showFillIcons && renderAction("changeFillStyle")}
 
       {(hasStrokeWidth(appState.activeTool.type) ||

+ 19 - 10
src/components/App.tsx

@@ -6167,20 +6167,29 @@ class App extends React.Component<AppProps, AppState> {
     type: "canvas" | "element" | "custom",
     source?: string,
   ): ContextMenuItems => {
-    const options: ContextMenuItems = [];
-    let addedCustom = false;
+    const custom: ContextMenuItems = [];
     this.actionManager
-      .getCustomActions({ data: { source } })
-      .forEach((action) => {
-        addedCustom = true;
-        options.push(action);
-      });
+      .getCustomActions({ data: { source: source ?? "" } })
+      .forEach((action) => custom.push(action));
     if (type === "custom") {
-      return options;
+      return custom;
     }
-    if (addedCustom) {
-      options.push(CONTEXT_MENU_SEPARATOR);
+    if (custom.length > 0) {
+      custom.push(CONTEXT_MENU_SEPARATOR);
     }
+    const standard: ContextMenuItems = this._getContextMenuItems(type).filter(
+      (item) =>
+        !item ||
+        item === CONTEXT_MENU_SEPARATOR ||
+        this.actionManager.isActionEnabled(item, { guardsOnly: true }),
+    );
+    return [...custom, ...standard];
+  };
+
+  private _getContextMenuItems = (
+    type: "canvas" | "element",
+  ): ContextMenuItems => {
+    const options: ContextMenuItems = [];
 
     options.push(actionCopyAsPng, actionCopyAsSvg);
 

+ 0 - 1
src/components/LayerUI.tsx

@@ -244,7 +244,6 @@ const LayerUI = ({
           appState={appState}
           elements={elements}
           renderAction={actionManager.renderAction}
-          getCustomActions={actionManager.getCustomActions}
         />
       </Island>
     </Section>

+ 0 - 1
src/components/MobileMenu.tsx

@@ -193,7 +193,6 @@ export const MobileMenu = ({
                 appState={appState}
                 elements={elements}
                 renderAction={actionManager.renderAction}
-                getCustomActions={actionManager.getCustomActions}
               />
             </Section>
           ) : null}