Simplify custom Actions: universal Action predicates instead of
action-specific guards.
This commit is contained in:
parent
512e506798
commit
c8d4e8c421
@ -1,25 +0,0 @@
|
|||||||
import { Action, ActionName, DisableFn, EnableFn } from "./types";
|
|
||||||
|
|
||||||
const disablers = {} as Record<ActionName, DisableFn[]>;
|
|
||||||
const enablers = {} as Record<Action["name"], EnableFn[]>;
|
|
||||||
|
|
||||||
export const getActionDisablers = () => disablers;
|
|
||||||
export const getActionEnablers = () => enablers;
|
|
||||||
|
|
||||||
export const registerDisableFn = (name: ActionName, disabler: DisableFn) => {
|
|
||||||
if (!(name in disablers)) {
|
|
||||||
disablers[name] = [] as DisableFn[];
|
|
||||||
}
|
|
||||||
if (!disablers[name].includes(disabler)) {
|
|
||||||
disablers[name].push(disabler);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const registerEnableFn = (name: Action["name"], enabler: EnableFn) => {
|
|
||||||
if (!(name in enablers)) {
|
|
||||||
enablers[name] = [] as EnableFn[];
|
|
||||||
}
|
|
||||||
if (!enablers[name].includes(enabler)) {
|
|
||||||
enablers[name].push(enabler);
|
|
||||||
}
|
|
||||||
};
|
|
@ -2,15 +2,12 @@ import React from "react";
|
|||||||
import {
|
import {
|
||||||
Action,
|
Action,
|
||||||
UpdaterFn,
|
UpdaterFn,
|
||||||
ActionName,
|
|
||||||
ActionResult,
|
ActionResult,
|
||||||
PanelComponentProps,
|
PanelComponentProps,
|
||||||
ActionSource,
|
ActionSource,
|
||||||
DisableFn,
|
ActionPredicateFn,
|
||||||
EnableFn,
|
|
||||||
isActionName,
|
isActionName,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { getActionDisablers, getActionEnablers } from "./guards";
|
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppClassProperties, AppState } from "../types";
|
import { AppClassProperties, AppState } from "../types";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
@ -44,10 +41,8 @@ const trackAction = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class ActionManager {
|
export class ActionManager {
|
||||||
actions = {} as Record<ActionName | Action["name"], Action>;
|
actions = {} as Record<Action["name"], Action>;
|
||||||
|
actionPredicates = [] as ActionPredicateFn[];
|
||||||
disablers = {} as Record<ActionName, DisableFn[]>;
|
|
||||||
enablers = {} as Record<Action["name"], EnableFn[]>;
|
|
||||||
|
|
||||||
updater: (actionResult: ActionResult | Promise<ActionResult>) => void;
|
updater: (actionResult: ActionResult | Promise<ActionResult>) => void;
|
||||||
|
|
||||||
@ -75,36 +70,9 @@ export class ActionManager {
|
|||||||
this.app = app;
|
this.app = app;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerActionGuards() {
|
registerActionPredicate(predicate: ActionPredicateFn) {
|
||||||
const disablers = getActionDisablers();
|
if (!this.actionPredicates.includes(predicate)) {
|
||||||
for (const d in disablers) {
|
this.actionPredicates.push(predicate);
|
||||||
const dName = d as ActionName;
|
|
||||||
disablers[dName].forEach((disabler) =>
|
|
||||||
this.registerDisableFn(dName, disabler),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const enablers = getActionEnablers();
|
|
||||||
for (const e in enablers) {
|
|
||||||
const eName = e as Action["name"];
|
|
||||||
enablers[e].forEach((enabler) => this.registerEnableFn(eName, enabler));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
registerDisableFn(name: ActionName, disabler: DisableFn) {
|
|
||||||
if (!(name in this.disablers)) {
|
|
||||||
this.disablers[name] = [] as DisableFn[];
|
|
||||||
}
|
|
||||||
if (!this.disablers[name].includes(disabler)) {
|
|
||||||
this.disablers[name].push(disabler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
registerEnableFn(name: Action["name"], enabler: EnableFn) {
|
|
||||||
if (!(name in this.enablers)) {
|
|
||||||
this.enablers[name] = [] as EnableFn[];
|
|
||||||
}
|
|
||||||
if (!this.enablers[name].includes(enabler)) {
|
|
||||||
this.enablers[name].push(enabler);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,10 +164,7 @@ export class ActionManager {
|
|||||||
/**
|
/**
|
||||||
* @param data additional data sent to the PanelComponent
|
* @param data additional data sent to the PanelComponent
|
||||||
*/
|
*/
|
||||||
renderAction = (
|
renderAction = (name: Action["name"], data?: PanelComponentProps["data"]) => {
|
||||||
name: ActionName | Action["name"],
|
|
||||||
data?: PanelComponentProps["data"],
|
|
||||||
) => {
|
|
||||||
const canvasActions = this.app.props.UIOptions.canvasActions;
|
const canvasActions = this.app.props.UIOptions.canvasActions;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -243,7 +208,7 @@ export class ActionManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
isActionEnabled = (
|
isActionEnabled = (
|
||||||
action: Action | ActionName,
|
action: Action,
|
||||||
opts?: {
|
opts?: {
|
||||||
elements?: readonly ExcalidrawElement[];
|
elements?: readonly ExcalidrawElement[];
|
||||||
data?: Record<string, any>;
|
data?: Record<string, any>;
|
||||||
@ -254,29 +219,19 @@ export class ActionManager {
|
|||||||
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 (
|
||||||
!opts?.guardsOnly &&
|
!opts?.guardsOnly &&
|
||||||
_action.predicate &&
|
action.predicate &&
|
||||||
!_action.predicate(elements, appState, this.app.props, this.app, data)
|
!action.predicate(elements, appState, this.app.props, this.app, data)
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
let enabled = true;
|
||||||
if (isActionName(_action.name)) {
|
this.actionPredicates.forEach((fn) => {
|
||||||
return !(
|
if (!fn(action, elements, appState, data)) {
|
||||||
_action.name in this.disablers &&
|
enabled = false;
|
||||||
this.disablers[_action.name].some((fn) =>
|
}
|
||||||
fn(elements, appState, _action.name as ActionName),
|
});
|
||||||
)
|
return enabled;
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
_action.name in this.enablers &&
|
|
||||||
this.enablers[_action.name].some((fn) =>
|
|
||||||
fn(elements, appState, _action.name),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,8 @@
|
|||||||
import { Action, isActionName } from "./types";
|
import { Action } from "./types";
|
||||||
|
|
||||||
let actions: readonly Action[] = [];
|
export let actions: readonly Action[] = [];
|
||||||
let customActions: readonly Action[] = [];
|
|
||||||
export const getCustomActions = () => customActions;
|
|
||||||
export const getActions = () => actions;
|
|
||||||
|
|
||||||
export const register = <T extends Action>(action: T) => {
|
export const register = <T extends Action>(action: T) => {
|
||||||
if (!isActionName(action.name)) {
|
|
||||||
customActions = customActions.concat(action);
|
|
||||||
}
|
|
||||||
actions = actions.concat(action);
|
actions = actions.concat(action);
|
||||||
return action as T & {
|
return action as T & {
|
||||||
keyTest?: unknown extends T["keyTest"] ? never : T["keyTest"];
|
keyTest?: unknown extends T["keyTest"] ? never : T["keyTest"];
|
||||||
|
@ -31,20 +31,13 @@ type ActionFn = (
|
|||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
) => ActionResult | Promise<ActionResult>;
|
) => ActionResult | Promise<ActionResult>;
|
||||||
|
|
||||||
// Return `true` to indicate the standard Action with name `actionName`
|
// Return `true` *unless* `Action` should be disabled
|
||||||
// should be disabled given `elements` and `appState`.
|
// given `elements`, `appState`, and optionally `data`.
|
||||||
export type DisableFn = (
|
export type ActionPredicateFn = (
|
||||||
|
action: Action,
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
actionName: ActionName,
|
data?: Record<string, any>,
|
||||||
) => boolean;
|
|
||||||
|
|
||||||
// Return `true` to indicate the custom Action with name `actionName`
|
|
||||||
// should be enabled given `elements` and `appState`.
|
|
||||||
export type EnableFn = (
|
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
appState: AppState,
|
|
||||||
actionName: Action["name"],
|
|
||||||
) => boolean;
|
) => boolean;
|
||||||
|
|
||||||
export type UpdaterFn = (res: ActionResult) => void;
|
export type UpdaterFn = (res: ActionResult) => void;
|
||||||
|
@ -38,7 +38,7 @@ import {
|
|||||||
} from "../actions";
|
} from "../actions";
|
||||||
import { createRedoAction, createUndoAction } from "../actions/actionHistory";
|
import { createRedoAction, createUndoAction } from "../actions/actionHistory";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { getActions } from "../actions/register";
|
import { actions } from "../actions/register";
|
||||||
import { ActionResult } from "../actions/types";
|
import { ActionResult } from "../actions/types";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import {
|
import {
|
||||||
@ -241,6 +241,7 @@ import {
|
|||||||
SubtypePrepFn,
|
SubtypePrepFn,
|
||||||
prepareSubtype,
|
prepareSubtype,
|
||||||
selectSubtype,
|
selectSubtype,
|
||||||
|
subtypeActionPredicate,
|
||||||
} from "../subtypes";
|
} from "../subtypes";
|
||||||
import {
|
import {
|
||||||
dataURLToFile,
|
dataURLToFile,
|
||||||
@ -490,12 +491,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
onSceneUpdated: this.onSceneUpdated,
|
onSceneUpdated: this.onSceneUpdated,
|
||||||
});
|
});
|
||||||
this.history = new History();
|
this.history = new History();
|
||||||
this.actionManager.registerAll(getActions());
|
this.actionManager.registerAll(actions);
|
||||||
|
|
||||||
this.actionManager.registerAction(createUndoAction(this.history));
|
this.actionManager.registerAction(createUndoAction(this.history));
|
||||||
this.actionManager.registerAction(createRedoAction(this.history));
|
this.actionManager.registerAction(createRedoAction(this.history));
|
||||||
// Call `this.addSubtype()` here for `@excalidraw/excalidraw`-specific subtypes
|
// Call `this.addSubtype()` here for `@excalidraw/excalidraw`-specific subtypes
|
||||||
this.actionManager.registerActionGuards();
|
this.actionManager.registerActionPredicate(subtypeActionPredicate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private addSubtype(record: SubtypeRecord, subtypePrepFn: SubtypePrepFn) {
|
private addSubtype(record: SubtypeRecord, subtypePrepFn: SubtypePrepFn) {
|
||||||
@ -527,7 +528,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (prep.actions) {
|
if (prep.actions) {
|
||||||
this.actionManager.registerAll(prep.actions);
|
this.actionManager.registerAll(prep.actions);
|
||||||
}
|
}
|
||||||
this.actionManager.registerActionGuards();
|
|
||||||
return prep;
|
return prep;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,13 +8,12 @@ import { getSelectedElements } from "./scene";
|
|||||||
import { AppState } from "./types";
|
import { AppState } from "./types";
|
||||||
import { registerAuxLangData } from "./i18n";
|
import { registerAuxLangData } from "./i18n";
|
||||||
|
|
||||||
import { Action, ActionName, DisableFn, EnableFn } from "./actions/types";
|
import { Action, ActionName, ActionPredicateFn } from "./actions/types";
|
||||||
import {
|
import {
|
||||||
CustomShortcutName,
|
CustomShortcutName,
|
||||||
registerCustomShortcuts,
|
registerCustomShortcuts,
|
||||||
} from "./actions/shortcuts";
|
} from "./actions/shortcuts";
|
||||||
import { register } from "./actions/register";
|
import { register } from "./actions/register";
|
||||||
import { registerDisableFn, registerEnableFn } from "./actions/guards";
|
|
||||||
import { hasBoundTextElement } from "./element/typeChecks";
|
import { hasBoundTextElement } from "./element/typeChecks";
|
||||||
import { getBoundTextElement } from "./element/textElement";
|
import { getBoundTextElement } from "./element/textElement";
|
||||||
|
|
||||||
@ -100,16 +99,16 @@ const isForSubtype = (
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isActionDisabled: DisableFn = function (elements, appState, actionName) {
|
export const subtypeActionPredicate: ActionPredicateFn = function (
|
||||||
return !isActionEnabled(elements, appState, actionName);
|
action,
|
||||||
};
|
elements,
|
||||||
|
appState,
|
||||||
const isActionEnabled: EnableFn = function (elements, appState, actionName) {
|
) {
|
||||||
// We always enable subtype actions. Also let through standard actions
|
// We always enable subtype actions. Also let through standard actions
|
||||||
// which no subtypes might have disabled.
|
// which no subtypes might have disabled.
|
||||||
if (
|
if (
|
||||||
isSubtypeName(actionName) ||
|
isSubtypeName(action.name) ||
|
||||||
(!isSubtypeActionName(actionName) && !isDisabledActionName(actionName))
|
(!isSubtypeActionName(action.name) && !isDisabledActionName(action.name))
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -121,13 +120,13 @@ const isActionEnabled: EnableFn = function (elements, appState, actionName) {
|
|||||||
? [appState.editingElement, ...selectedElements]
|
? [appState.editingElement, ...selectedElements]
|
||||||
: selectedElements;
|
: selectedElements;
|
||||||
// Now handle actions added by subtypes
|
// Now handle actions added by subtypes
|
||||||
if (isSubtypeActionName(actionName)) {
|
if (isSubtypeActionName(action.name)) {
|
||||||
// Has any ExcalidrawElement enabled this actionName through having
|
// Has any ExcalidrawElement enabled this actionName through having
|
||||||
// its subtype?
|
// its subtype?
|
||||||
return (
|
return (
|
||||||
chosen.some((el) => {
|
chosen.some((el) => {
|
||||||
const e = hasBoundTextElement(el) ? getBoundTextElement(el)! : el;
|
const e = hasBoundTextElement(el) ? getBoundTextElement(el)! : el;
|
||||||
return isForSubtype(e.subtype, actionName, true);
|
return isForSubtype(e.subtype, action.name, true);
|
||||||
}) ||
|
}) ||
|
||||||
// Or has any active subtype enabled this actionName?
|
// Or has any active subtype enabled this actionName?
|
||||||
(appState.activeSubtypes !== undefined &&
|
(appState.activeSubtypes !== undefined &&
|
||||||
@ -135,20 +134,20 @@ const isActionEnabled: EnableFn = function (elements, appState, actionName) {
|
|||||||
if (!isValidSubtype(subtype, appState.activeTool.type)) {
|
if (!isValidSubtype(subtype, appState.activeTool.type)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return isForSubtype(subtype, actionName, true);
|
return isForSubtype(subtype, action.name, true);
|
||||||
})) ||
|
})) ||
|
||||||
alwaysEnabledMap.some((value) => {
|
alwaysEnabledMap.some((value) => {
|
||||||
return value.actions.includes(actionName);
|
return value.actions.includes(action.name);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Now handle standard actions disabled by subtypes
|
// Now handle standard actions disabled by subtypes
|
||||||
if (isDisabledActionName(actionName)) {
|
if (isDisabledActionName(action.name)) {
|
||||||
return (
|
return (
|
||||||
// Has every ExcalidrawElement not disabled this actionName?
|
// Has every ExcalidrawElement not disabled this actionName?
|
||||||
(chosen.every((el) => {
|
(chosen.every((el) => {
|
||||||
const e = hasBoundTextElement(el) ? getBoundTextElement(el)! : el;
|
const e = hasBoundTextElement(el) ? getBoundTextElement(el)! : el;
|
||||||
return !isForSubtype(e.subtype, actionName, false);
|
return !isForSubtype(e.subtype, action.name, false);
|
||||||
}) &&
|
}) &&
|
||||||
// And has every active subtype not disabled this actionName?
|
// And has every active subtype not disabled this actionName?
|
||||||
(appState.activeSubtypes === undefined ||
|
(appState.activeSubtypes === undefined ||
|
||||||
@ -156,7 +155,7 @@ const isActionEnabled: EnableFn = function (elements, appState, actionName) {
|
|||||||
if (!isValidSubtype(subtype, appState.activeTool.type)) {
|
if (!isValidSubtype(subtype, appState.activeTool.type)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return !isForSubtype(subtype, actionName, false);
|
return !isForSubtype(subtype, action.name, false);
|
||||||
}))) ||
|
}))) ||
|
||||||
// Or can we find an ExcalidrawElement without a valid subtype
|
// Or can we find an ExcalidrawElement without a valid subtype
|
||||||
// which would disable this action if it had a valid subtype?
|
// which would disable this action if it had a valid subtype?
|
||||||
@ -166,14 +165,14 @@ const isActionEnabled: EnableFn = function (elements, appState, actionName) {
|
|||||||
(value) =>
|
(value) =>
|
||||||
value.parentType === e.type &&
|
value.parentType === e.type &&
|
||||||
!isValidSubtype(e.subtype, e.type) &&
|
!isValidSubtype(e.subtype, e.type) &&
|
||||||
isForSubtype(value.subtype, actionName, false),
|
isForSubtype(value.subtype, action.name, false),
|
||||||
);
|
);
|
||||||
}) ||
|
}) ||
|
||||||
chosen.some((el) => {
|
chosen.some((el) => {
|
||||||
const e = hasBoundTextElement(el) ? getBoundTextElement(el)! : el;
|
const e = hasBoundTextElement(el) ? getBoundTextElement(el)! : el;
|
||||||
return (
|
return (
|
||||||
// Would the subtype of e by inself disable this action?
|
// Would the subtype of e by inself disable this action?
|
||||||
isForSubtype(e.subtype, actionName, false) &&
|
isForSubtype(e.subtype, action.name, false) &&
|
||||||
// Can we find an ExcalidrawElement which could have the same subtype
|
// Can we find an ExcalidrawElement which could have the same subtype
|
||||||
// as e but whose subtype does not disable this action?
|
// as e but whose subtype does not disable this action?
|
||||||
chosen.some((el) => {
|
chosen.some((el) => {
|
||||||
@ -184,7 +183,7 @@ const isActionEnabled: EnableFn = function (elements, appState, actionName) {
|
|||||||
parentTypeMap
|
parentTypeMap
|
||||||
.filter((val) => val.subtype === e.subtype)
|
.filter((val) => val.subtype === e.subtype)
|
||||||
.some((val) => val.parentType === e2.type) &&
|
.some((val) => val.parentType === e2.type) &&
|
||||||
!isForSubtype(e2.subtype, actionName, false)
|
!isForSubtype(e2.subtype, action.name, false)
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -381,13 +380,6 @@ export const prepareSubtype = (
|
|||||||
onSubtypeLoaded,
|
onSubtypeLoaded,
|
||||||
);
|
);
|
||||||
|
|
||||||
record.disabledNames?.forEach((name) => {
|
|
||||||
registerDisableFn(name, isActionDisabled);
|
|
||||||
});
|
|
||||||
record.actionNames?.forEach((name) => {
|
|
||||||
registerEnableFn(name, isActionEnabled);
|
|
||||||
});
|
|
||||||
registerEnableFn(record.subtype, isActionEnabled);
|
|
||||||
// Register the subtype's methods
|
// Register the subtype's methods
|
||||||
addSubtypeMethods(record.subtype, methods);
|
addSubtypeMethods(record.subtype, methods);
|
||||||
return { actions, methods };
|
return { actions, methods };
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
import { API } from "./helpers/api";
|
import { API } from "./helpers/api";
|
||||||
|
import { render } from "./test-utils";
|
||||||
|
import ExcalidrawApp from "../excalidraw-app";
|
||||||
import {
|
import {
|
||||||
CustomShortcutName,
|
CustomShortcutName,
|
||||||
getShortcutFromShortcutName,
|
getShortcutFromShortcutName,
|
||||||
registerCustomShortcuts,
|
registerCustomShortcuts,
|
||||||
} from "../actions/shortcuts";
|
} from "../actions/shortcuts";
|
||||||
import { Action, ActionName, DisableFn, EnableFn } from "../actions/types";
|
import { Action, ActionPredicateFn, ActionResult } from "../actions/types";
|
||||||
import {
|
import {
|
||||||
getActionDisablers,
|
actionChangeFontFamily,
|
||||||
getActionEnablers,
|
actionChangeFontSize,
|
||||||
registerDisableFn,
|
} from "../actions/actionProperties";
|
||||||
registerEnableFn,
|
import { isTextElement } from "../element";
|
||||||
} from "../actions/guards";
|
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@ -25,61 +26,60 @@ describe("regression tests", () => {
|
|||||||
expect(getShortcutFromShortcutName("test")).toBe("Ctrl+1");
|
expect(getShortcutFromShortcutName("test")).toBe("Ctrl+1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should follow action guards", () => {
|
it("should apply universal action predicates", async () => {
|
||||||
|
await render(<ExcalidrawApp />);
|
||||||
// Create the test elements
|
// Create the test elements
|
||||||
const text1 = API.createElement({ type: "rectangle", id: "A", y: 0 });
|
const el1 = API.createElement({ type: "rectangle", id: "A", y: 0 });
|
||||||
const text2 = API.createElement({ type: "rectangle", id: "B", y: 30 });
|
const el2 = API.createElement({ type: "rectangle", id: "B", y: 30 });
|
||||||
const text3 = API.createElement({ type: "rectangle", id: "C", y: 60 });
|
const el3 = API.createElement({ type: "text", id: "C", y: 60 });
|
||||||
const el12: ExcalidrawElement[] = [text1, text2];
|
const el12: ExcalidrawElement[] = [el1, el2];
|
||||||
const el13: ExcalidrawElement[] = [text1, text3];
|
const el13: ExcalidrawElement[] = [el1, el3];
|
||||||
const el23: ExcalidrawElement[] = [text2, text3];
|
const el23: ExcalidrawElement[] = [el2, el3];
|
||||||
const el123: ExcalidrawElement[] = [text1, text2, text3];
|
const el123: ExcalidrawElement[] = [el1, el2, el3];
|
||||||
// Set up the custom Action enablers
|
// Set up the custom Action enablers
|
||||||
const enableName = "custom" as Action["name"];
|
const enableName = "custom" as Action["name"];
|
||||||
const enabler: EnableFn = function (elements) {
|
const enableAction: Action = {
|
||||||
if (elements.some((el) => el.y === 30)) {
|
name: enableName,
|
||||||
|
perform: (): ActionResult => {
|
||||||
|
return {} as ActionResult;
|
||||||
|
},
|
||||||
|
trackEvent: false,
|
||||||
|
};
|
||||||
|
const enabler: ActionPredicateFn = function (action, elements) {
|
||||||
|
if (action.name !== enableName || elements.some((el) => el.y === 30)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
registerEnableFn(enableName, enabler);
|
|
||||||
// Set up the standard Action disablers
|
// Set up the standard Action disablers
|
||||||
const disableName1 = "changeFontFamily" as ActionName;
|
const disabled1 = actionChangeFontFamily;
|
||||||
const disableName2 = "changeFontSize" as ActionName;
|
const disabled2 = actionChangeFontSize;
|
||||||
const disabler: DisableFn = function (elements) {
|
const disabler: ActionPredicateFn = function (action, elements) {
|
||||||
if (elements.some((el) => el.y === 0)) {
|
if (
|
||||||
return true;
|
action.name === disabled2.name &&
|
||||||
|
elements.some((el) => el.y === 0 || isTextElement(el))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return false;
|
return true;
|
||||||
};
|
};
|
||||||
registerDisableFn(disableName1, disabler);
|
|
||||||
// Test the custom Action enablers
|
// Test the custom Action enablers
|
||||||
const enablers = getActionEnablers();
|
const am = h.app.actionManager;
|
||||||
const isCustomEnabled = function (
|
am.registerActionPredicate(enabler);
|
||||||
elements: ExcalidrawElement[],
|
expect(am.isActionEnabled(enableAction, { elements: el12 })).toBe(true);
|
||||||
name: string,
|
expect(am.isActionEnabled(enableAction, { elements: el13 })).toBe(false);
|
||||||
) {
|
expect(am.isActionEnabled(enableAction, { elements: el23 })).toBe(true);
|
||||||
return (
|
expect(am.isActionEnabled(disabled1, { elements: el12 })).toBe(true);
|
||||||
name in enablers &&
|
expect(am.isActionEnabled(disabled1, { elements: el13 })).toBe(true);
|
||||||
enablers[name].some((enabler) => enabler(elements, h.state, name))
|
expect(am.isActionEnabled(disabled1, { elements: el23 })).toBe(true);
|
||||||
);
|
|
||||||
};
|
|
||||||
expect(isCustomEnabled(el12, enableName)).toBe(true);
|
|
||||||
expect(isCustomEnabled(el13, enableName)).toBe(false);
|
|
||||||
expect(isCustomEnabled(el23, enableName)).toBe(true);
|
|
||||||
// Test the standard Action disablers
|
// Test the standard Action disablers
|
||||||
const disablers = getActionDisablers();
|
am.registerActionPredicate(disabler);
|
||||||
const isStandardDisabled = function (
|
expect(am.isActionEnabled(disabled1, { elements: el123 })).toBe(true);
|
||||||
elements: ExcalidrawElement[],
|
expect(am.isActionEnabled(disabled2, { elements: [el1] })).toBe(false);
|
||||||
name: ActionName,
|
expect(am.isActionEnabled(disabled2, { elements: [el2] })).toBe(true);
|
||||||
) {
|
expect(am.isActionEnabled(disabled2, { elements: [el3] })).toBe(false);
|
||||||
return (
|
expect(am.isActionEnabled(disabled2, { elements: el12 })).toBe(false);
|
||||||
name in disablers &&
|
expect(am.isActionEnabled(disabled2, { elements: el23 })).toBe(false);
|
||||||
disablers[name].some((disabler) => disabler(elements, h.state, name))
|
expect(am.isActionEnabled(disabled2, { elements: el13 })).toBe(false);
|
||||||
);
|
|
||||||
};
|
|
||||||
expect(isStandardDisabled(el12, disableName1)).toBe(true);
|
|
||||||
expect(isStandardDisabled(el23, disableName1)).toBe(false);
|
|
||||||
expect(isStandardDisabled(el123, disableName2)).toBe(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
SubtypeRecord,
|
SubtypeRecord,
|
||||||
prepareSubtype,
|
prepareSubtype,
|
||||||
selectSubtype,
|
selectSubtype,
|
||||||
|
subtypeActionPredicate,
|
||||||
} from "../../subtypes";
|
} from "../../subtypes";
|
||||||
import {
|
import {
|
||||||
maybeGetSubtypeProps,
|
maybeGetSubtypeProps,
|
||||||
@ -36,6 +37,7 @@ const { h } = window;
|
|||||||
|
|
||||||
export class API {
|
export class API {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
h.app.actionManager.registerActionPredicate(subtypeActionPredicate);
|
||||||
if (true) {
|
if (true) {
|
||||||
// Call `prepareSubtype()` here for `@excalidraw/excalidraw`-specific subtypes
|
// Call `prepareSubtype()` here for `@excalidraw/excalidraw`-specific subtypes
|
||||||
}
|
}
|
||||||
@ -45,7 +47,6 @@ export class API {
|
|||||||
const prep = prepareSubtype(record, subtypePrepFn);
|
const prep = prepareSubtype(record, subtypePrepFn);
|
||||||
if (prep.actions) {
|
if (prep.actions) {
|
||||||
h.app.actionManager.registerAll(prep.actions);
|
h.app.actionManager.registerAll(prep.actions);
|
||||||
h.app.actionManager.registerActionGuards();
|
|
||||||
}
|
}
|
||||||
return prep;
|
return prep;
|
||||||
};
|
};
|
||||||
|
@ -24,9 +24,11 @@ import { getFontString, getShortcutKey } from "../utils";
|
|||||||
import * as textElementUtils from "../element/textElement";
|
import * as textElementUtils from "../element/textElement";
|
||||||
import { isTextElement } from "../element";
|
import { isTextElement } from "../element";
|
||||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||||
import { Action } from "../actions/types";
|
import { Action, ActionName } from "../actions/types";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { getShortcutFromShortcutName } from "../actions/shortcuts";
|
import { getShortcutFromShortcutName } from "../actions/shortcuts";
|
||||||
|
import { actionChangeSloppiness } from "../actions";
|
||||||
|
import { actionChangeRoundness } from "../actions/actionProperties";
|
||||||
|
|
||||||
const MW = 200;
|
const MW = 200;
|
||||||
const TWIDTH = 200;
|
const TWIDTH = 200;
|
||||||
@ -60,13 +62,13 @@ const testSubtypeIcon = ({ theme }: { theme: Theme }) =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const TEST_ACTION = "testAction";
|
const TEST_ACTION = "testAction";
|
||||||
const TEST_DISABLE1 = "changeSloppiness";
|
const TEST_DISABLE1 = actionChangeSloppiness;
|
||||||
const TEST_DISABLE3 = "changeRoundness";
|
const TEST_DISABLE3 = actionChangeRoundness;
|
||||||
|
|
||||||
const test1: SubtypeRecord = {
|
const test1: SubtypeRecord = {
|
||||||
subtype: "test",
|
subtype: "test",
|
||||||
parents: ["line", "arrow", "rectangle", "diamond", "ellipse"],
|
parents: ["line", "arrow", "rectangle", "diamond", "ellipse"],
|
||||||
disabledNames: [TEST_DISABLE1],
|
disabledNames: [TEST_DISABLE1.name as ActionName],
|
||||||
actionNames: [TEST_ACTION],
|
actionNames: [TEST_ACTION],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -106,7 +108,7 @@ const test3: SubtypeRecord = {
|
|||||||
testShortcut: [getShortcutKey("Shift+T")],
|
testShortcut: [getShortcutKey("Shift+T")],
|
||||||
},
|
},
|
||||||
alwaysEnabledNames: ["test3Always"],
|
alwaysEnabledNames: ["test3Always"],
|
||||||
disabledNames: [TEST_DISABLE3],
|
disabledNames: [TEST_DISABLE3.name as ActionName],
|
||||||
};
|
};
|
||||||
|
|
||||||
const test3Button = SubtypeButton(
|
const test3Button = SubtypeButton(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user