fix: Use a narrower type for custom shortcut names and reduce hard-coding.
This commit is contained in:
parent
f71ded4bf9
commit
9642a6e756
@ -2,11 +2,12 @@ import { isDarwin } from "../constants";
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { SubtypeOf } from "../utility-types";
|
import { SubtypeOf } from "../utility-types";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
import { ActionName } from "./types";
|
import { ActionName, CustomActionName } from "./types";
|
||||||
|
|
||||||
export type ShortcutName =
|
export type ShortcutName =
|
||||||
| SubtypeOf<
|
| SubtypeOf<
|
||||||
ActionName,
|
ActionName,
|
||||||
|
| CustomActionName
|
||||||
| "toggleTheme"
|
| "toggleTheme"
|
||||||
| "loadScene"
|
| "loadScene"
|
||||||
| "clearCanvas"
|
| "clearCanvas"
|
||||||
@ -40,6 +41,15 @@ export type ShortcutName =
|
|||||||
| "saveScene"
|
| "saveScene"
|
||||||
| "imageExport";
|
| "imageExport";
|
||||||
|
|
||||||
|
export const registerCustomShortcuts = (
|
||||||
|
shortcuts: Record<CustomActionName, string[]>,
|
||||||
|
) => {
|
||||||
|
for (const key in shortcuts) {
|
||||||
|
const shortcut = key as CustomActionName;
|
||||||
|
shortcutMap[shortcut] = shortcuts[shortcut];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const shortcutMap: Record<ShortcutName, string[]> = {
|
const shortcutMap: Record<ShortcutName, string[]> = {
|
||||||
toggleTheme: [getShortcutKey("Shift+Alt+D")],
|
toggleTheme: [getShortcutKey("Shift+Alt+D")],
|
||||||
saveScene: [getShortcutKey("CtrlOrCmd+S")],
|
saveScene: [getShortcutKey("CtrlOrCmd+S")],
|
||||||
@ -85,23 +95,8 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
|||||||
toggleElementLock: [getShortcutKey("CtrlOrCmd+Shift+L")],
|
toggleElementLock: [getShortcutKey("CtrlOrCmd+Shift+L")],
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CustomShortcutName = string;
|
export const getShortcutFromShortcutName = (name: ShortcutName) => {
|
||||||
|
const shortcuts = shortcutMap[name];
|
||||||
let customShortcutMap: Record<CustomShortcutName, string[]> = {};
|
|
||||||
|
|
||||||
export const registerCustomShortcuts = (
|
|
||||||
shortcuts: Record<CustomShortcutName, string[]>,
|
|
||||||
) => {
|
|
||||||
customShortcutMap = { ...customShortcutMap, ...shortcuts };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getShortcutFromShortcutName = (
|
|
||||||
name: ShortcutName | CustomShortcutName,
|
|
||||||
) => {
|
|
||||||
const shortcuts =
|
|
||||||
name in customShortcutMap
|
|
||||||
? customShortcutMap[name as CustomShortcutName]
|
|
||||||
: shortcutMap[name as ShortcutName];
|
|
||||||
// if multiple shortcuts available, take the first one
|
// if multiple shortcuts available, take the first one
|
||||||
return shortcuts && shortcuts.length > 0 ? shortcuts[0] : "";
|
return shortcuts && shortcuts.length > 0 ? shortcuts[0] : "";
|
||||||
};
|
};
|
||||||
|
@ -44,8 +44,11 @@ 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;
|
||||||
|
|
||||||
|
export const makeCustomActionName = (name: string) =>
|
||||||
|
`custom.${name}` as CustomActionName;
|
||||||
|
export type CustomActionName = `custom.${string}`;
|
||||||
export type ActionName =
|
export type ActionName =
|
||||||
| `custom.${string}`
|
| CustomActionName
|
||||||
| "copy"
|
| "copy"
|
||||||
| "cut"
|
| "cut"
|
||||||
| "paste"
|
| "paste"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { getShortcutKey, updateActiveTool } from "../utils";
|
import { getShortcutKey, updateActiveTool } from "../utils";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { Action } from "../actions/types";
|
import { Action, makeCustomActionName } from "../actions/types";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {
|
import {
|
||||||
Subtype,
|
Subtype,
|
||||||
@ -29,7 +29,7 @@ export const SubtypeButton = (
|
|||||||
const keyTest: Action["keyTest"] =
|
const keyTest: Action["keyTest"] =
|
||||||
key !== undefined ? (event) => event.code === `Key${key}` : undefined;
|
key !== undefined ? (event) => event.code === `Key${key}` : undefined;
|
||||||
const subtypeAction: Action = {
|
const subtypeAction: Action = {
|
||||||
name: `custom.${subtype}`,
|
name: makeCustomActionName(subtype),
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
predicate: (...rest) => rest[4]?.subtype === subtype,
|
predicate: (...rest) => rest[4]?.subtype === subtype,
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
@ -159,7 +159,7 @@ export const SubtypeToggles = () => {
|
|||||||
>
|
>
|
||||||
{getSubtypeNames().map((subtype) =>
|
{getSubtypeNames().map((subtype) =>
|
||||||
am.renderAction(
|
am.renderAction(
|
||||||
`custom.${subtype}`,
|
makeCustomActionName(subtype),
|
||||||
hasAlwaysEnabledActions(subtype) ? { onContextMenu } : {},
|
hasAlwaysEnabledActions(subtype) ? { onContextMenu } : {},
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
|
@ -5,11 +5,14 @@ import { getSelectedElements } from "../../scene";
|
|||||||
import { AppState, ExcalidrawImperativeAPI } from "../../types";
|
import { AppState, ExcalidrawImperativeAPI } from "../../types";
|
||||||
import { registerAuxLangData } from "../../i18n";
|
import { registerAuxLangData } from "../../i18n";
|
||||||
|
|
||||||
import { Action, ActionName, ActionPredicateFn } from "../../actions/types";
|
|
||||||
import {
|
import {
|
||||||
CustomShortcutName,
|
Action,
|
||||||
registerCustomShortcuts,
|
ActionName,
|
||||||
} from "../../actions/shortcuts";
|
ActionPredicateFn,
|
||||||
|
CustomActionName,
|
||||||
|
makeCustomActionName,
|
||||||
|
} from "../../actions/types";
|
||||||
|
import { registerCustomShortcuts } from "../../actions/shortcuts";
|
||||||
import { register } from "../../actions/register";
|
import { register } from "../../actions/register";
|
||||||
import { hasBoundTextElement, isTextElement } from "../typeChecks";
|
import { hasBoundTextElement, isTextElement } from "../typeChecks";
|
||||||
import {
|
import {
|
||||||
@ -44,7 +47,7 @@ export type SubtypeRecord = Readonly<{
|
|||||||
parents: readonly ExcalidrawElement["type"][];
|
parents: readonly ExcalidrawElement["type"][];
|
||||||
actionNames?: readonly SubtypeActionName[];
|
actionNames?: readonly SubtypeActionName[];
|
||||||
disabledNames?: readonly DisabledActionName[];
|
disabledNames?: readonly DisabledActionName[];
|
||||||
shortcutMap?: Record<CustomShortcutName, string[]>;
|
shortcutMap?: Record<string, string[]>;
|
||||||
alwaysEnabledNames?: readonly SubtypeActionName[];
|
alwaysEnabledNames?: readonly SubtypeActionName[];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
@ -373,8 +376,8 @@ export const prepareSubtype = (
|
|||||||
...subtypeActionMap,
|
...subtypeActionMap,
|
||||||
{
|
{
|
||||||
subtype,
|
subtype,
|
||||||
actions: record.actionNames.map(
|
actions: record.actionNames.map((actionName) =>
|
||||||
(actionName) => `custom.${actionName}` as ActionName,
|
makeCustomActionName(actionName),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -390,14 +393,19 @@ export const prepareSubtype = (
|
|||||||
...alwaysEnabledMap,
|
...alwaysEnabledMap,
|
||||||
{
|
{
|
||||||
subtype,
|
subtype,
|
||||||
actions: record.alwaysEnabledNames.map(
|
actions: record.alwaysEnabledNames.map((actionName) =>
|
||||||
(actionName) => `custom.${actionName}` as ActionName,
|
makeCustomActionName(actionName),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (record.shortcutMap) {
|
const customShortcutMap = record.shortcutMap;
|
||||||
registerCustomShortcuts(record.shortcutMap);
|
if (customShortcutMap) {
|
||||||
|
const shortcutMap: Record<CustomActionName, string[]> = {};
|
||||||
|
for (const key in customShortcutMap) {
|
||||||
|
shortcutMap[makeCustomActionName(key)] = customShortcutMap[key];
|
||||||
|
}
|
||||||
|
registerCustomShortcuts(shortcutMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare the subtype
|
// Prepare the subtype
|
||||||
|
@ -4,11 +4,16 @@ import { API } from "./helpers/api";
|
|||||||
import { render } from "./test-utils";
|
import { render } from "./test-utils";
|
||||||
import { Excalidraw } from "../packages/excalidraw/index";
|
import { Excalidraw } from "../packages/excalidraw/index";
|
||||||
import {
|
import {
|
||||||
CustomShortcutName,
|
|
||||||
getShortcutFromShortcutName,
|
getShortcutFromShortcutName,
|
||||||
registerCustomShortcuts,
|
registerCustomShortcuts,
|
||||||
} from "../actions/shortcuts";
|
} from "../actions/shortcuts";
|
||||||
import { Action, ActionPredicateFn, ActionResult } from "../actions/types";
|
import {
|
||||||
|
Action,
|
||||||
|
ActionPredicateFn,
|
||||||
|
ActionResult,
|
||||||
|
CustomActionName,
|
||||||
|
makeCustomActionName,
|
||||||
|
} from "../actions/types";
|
||||||
import {
|
import {
|
||||||
actionChangeFontFamily,
|
actionChangeFontFamily,
|
||||||
actionChangeFontSize,
|
actionChangeFontSize,
|
||||||
@ -19,11 +24,14 @@ const { h } = window;
|
|||||||
|
|
||||||
describe("regression tests", () => {
|
describe("regression tests", () => {
|
||||||
it("should retrieve custom shortcuts", () => {
|
it("should retrieve custom shortcuts", () => {
|
||||||
const shortcuts: Record<CustomShortcutName, string[]> = {
|
const shortcutName = makeCustomActionName("test");
|
||||||
test: [getShortcutKey("CtrlOrCmd+1"), getShortcutKey("CtrlOrCmd+2")],
|
const shortcuts: Record<CustomActionName, string[]> = {};
|
||||||
};
|
shortcuts[shortcutName] = [
|
||||||
|
getShortcutKey("CtrlOrCmd+1"),
|
||||||
|
getShortcutKey("CtrlOrCmd+2"),
|
||||||
|
];
|
||||||
registerCustomShortcuts(shortcuts);
|
registerCustomShortcuts(shortcuts);
|
||||||
expect(getShortcutFromShortcutName("test")).toBe("Ctrl+1");
|
expect(getShortcutFromShortcutName(shortcutName)).toBe("Ctrl+1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should apply universal action predicates", async () => {
|
it("should apply universal action predicates", async () => {
|
||||||
|
@ -32,7 +32,7 @@ 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, ActionName } from "../actions/types";
|
import { Action, ActionName, makeCustomActionName } 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 { actionChangeSloppiness } from "../actions";
|
||||||
@ -81,7 +81,7 @@ const test1: SubtypeRecord = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const testAction: Action = {
|
const testAction: Action = {
|
||||||
name: `custom.${TEST_ACTION}`,
|
name: makeCustomActionName(TEST_ACTION),
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
return {
|
return {
|
||||||
@ -338,7 +338,9 @@ describe("subtypes", () => {
|
|||||||
expect(test3Methods?.clean).toBeUndefined();
|
expect(test3Methods?.clean).toBeUndefined();
|
||||||
});
|
});
|
||||||
it("should register custom shortcuts", async () => {
|
it("should register custom shortcuts", async () => {
|
||||||
expect(getShortcutFromShortcutName("testShortcut")).toBe("Shift+T");
|
expect(
|
||||||
|
getShortcutFromShortcutName(makeCustomActionName("testShortcut")),
|
||||||
|
).toBe("Shift+T");
|
||||||
});
|
});
|
||||||
it("should correctly validate", async () => {
|
it("should correctly validate", async () => {
|
||||||
test1.parents.forEach((p) => {
|
test1.parents.forEach((p) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user