Filter context menu items through isActionEnabled.

This commit is contained in:
Daniel J. Geiger 2023-01-08 20:00:46 -06:00
parent 01432813a6
commit a5bd54b86d
5 changed files with 51 additions and 35 deletions

View File

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

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

View File

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

View File

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

View File

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