@witchcraft/spellcraft 0.0.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -14
- package/dist/core/createManagerOptions.js +1 -0
- package/dist/core/createShortcuts.d.ts +1 -1
- package/dist/core/setShortcutsProp.js +3 -3
- package/dist/defaults/defaultConditionEquals.d.ts +2 -16
- package/dist/defaults/defaultConditionEquals.js +1 -1
- package/dist/helpers/doesShortcutConflict.d.ts +1 -1
- package/dist/helpers/doesShortcutConflict.js +1 -1
- package/dist/helpers/equalsCommand.js +1 -1
- package/dist/helpers/equalsShortcut.d.ts +6 -4
- package/dist/helpers/equalsShortcut.js +5 -2
- package/dist/helpers/getKeyCodesFromKeyIds.d.ts +12 -0
- package/dist/helpers/getKeyCodesFromKeyIds.js +6 -0
- package/dist/helpers/getKeyCodesFromKeys.d.ts +13 -0
- package/dist/helpers/getKeyCodesFromKeys.js +16 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -2
- package/dist/runtime/types.d.ts +3 -4
- package/dist/types/condition.d.ts +3 -1
- package/dist/types/manager.d.ts +12 -1
- package/dist/types/shortcuts.d.ts +2 -2
- package/package.json +27 -25
- package/src/core/createManagerOptions.ts +1 -0
- package/src/core/createShortcuts.ts +1 -1
- package/src/core/setShortcutsProp.ts +3 -3
- package/src/defaults/defaultConditionEquals.ts +4 -17
- package/src/helpers/doesShortcutConflict.ts +2 -2
- package/src/helpers/equalsCommand.ts +1 -1
- package/src/helpers/equalsShortcut.ts +17 -5
- package/src/helpers/getKeyCodesFromKeyIds.ts +20 -0
- package/src/helpers/getKeyCodesFromKeys.ts +28 -0
- package/src/runtime/types.ts +18 -4
- package/src/types/condition.ts +3 -1
- package/src/types/manager.ts +13 -1
- package/src/types/shortcuts.ts +2 -2
package/README.md
CHANGED
|
@@ -123,7 +123,7 @@ You can choose in your execute function what exactly to do at that point for bot
|
|
|
123
123
|
|
|
124
124
|
If non-modifier keys are still being held at this point, the manager will not allow triggering a shortcut until they are released (see `state.isAwaitingKeyup`). Modifiers are not affect by this. We usually want the user to be able to keep the modifier pressed and do, for example, `Ctrl+B` then `Ctrl+I` to bold and italicize text, without having to release `Ctrl`, only `B` and `I`.
|
|
125
125
|
|
|
126
|
-
## Errors and
|
|
126
|
+
## Errors and Checking if Actions can be Performed
|
|
127
127
|
|
|
128
128
|
Note the use of `unwrap()`. Because many actions can throw "soft" errors, to better help deal with all the errors the library uses a Result monad in most of the return types. `unwrap` is like rust's unwrap and will throw the error if there was one, otherwise "unwrap" and return the value within.
|
|
129
129
|
|
|
@@ -278,12 +278,58 @@ export { }
|
|
|
278
278
|
```
|
|
279
279
|
|
|
280
280
|
Additionally, when you create a condition, you can pass a function to `parse` it and add these needed properties:
|
|
281
|
+
|
|
282
|
+
You'll probably want to create a wrapping function to do this. Here's an example with the expressit library:
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
<details>
|
|
286
|
+
|
|
287
|
+
<summary>Click to Expand</summary>
|
|
288
|
+
|
|
281
289
|
```ts
|
|
282
|
-
|
|
283
|
-
|
|
290
|
+
import { ShortcutContextParser } from "@witchcraft/expressit/examples/ShortcutContextParser"
|
|
291
|
+
import { createCondition as _createCondition } from "@witchcraft/spellcraft"
|
|
292
|
+
|
|
293
|
+
const dummyContext = { a: { }, b: { c: {} } }
|
|
294
|
+
const shortcutParser = new ShortcutContextParser(dummyContext) // see docs for details
|
|
295
|
+
function parser(text: string, _: Condition) {
|
|
296
|
+
const res = shortcutsParser.parse(text)
|
|
297
|
+
// === undefined is to narrow out the ErrorToken type
|
|
298
|
+
if (!res.valid || res.type === undefined) {
|
|
299
|
+
throw new Error(`Shortcut context parser failed to parse condition ${_.text}`)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
_.ast = res
|
|
284
303
|
return _
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Creates a condition with the ast saved in it. */
|
|
307
|
+
function createCondition(text: string) {
|
|
308
|
+
return _createCondition(text, parser)
|
|
309
|
+
}
|
|
310
|
+
const condition = createCondition("a || b")
|
|
311
|
+
condition.ast // should exist
|
|
312
|
+
|
|
313
|
+
// tell the manager how to evaluare the condition:
|
|
314
|
+
const manager = createmanager({
|
|
315
|
+
context: createcontext<context<map<string, boolean>>>(new map()),
|
|
316
|
+
options: {
|
|
317
|
+
evaluatecondition(condition, context) {
|
|
318
|
+
if (condition.ast === undefined) throw new Error("condition ast is undefined")
|
|
319
|
+
|
|
320
|
+
const res = condition.ast.valid
|
|
321
|
+
? conditionParser.evaluate(
|
|
322
|
+
conditionParser.normalize(condition.ast),
|
|
323
|
+
context.value.isActive
|
|
324
|
+
)
|
|
325
|
+
: false
|
|
326
|
+
return res
|
|
327
|
+
},
|
|
328
|
+
}
|
|
285
329
|
})
|
|
330
|
+
|
|
286
331
|
```
|
|
332
|
+
</details>
|
|
287
333
|
|
|
288
334
|
## Contexts
|
|
289
335
|
|
|
@@ -292,10 +338,10 @@ Similarly with contexts, you can use any sort of object or type that you like.
|
|
|
292
338
|
You can tell the manager it's type when you create it. For example, say we wanted to use a map:
|
|
293
339
|
|
|
294
340
|
```ts
|
|
295
|
-
const manager =
|
|
296
|
-
context:
|
|
341
|
+
const manager = createmanager({
|
|
342
|
+
context: createcontext<context<map<string, boolean>>>(new map()),
|
|
297
343
|
options: {
|
|
298
|
-
|
|
344
|
+
evaluatecondition(condition, context) {
|
|
299
345
|
// context is now correctly typed
|
|
300
346
|
return context.value.has(condition.text)
|
|
301
347
|
},
|
|
@@ -308,10 +354,10 @@ const manager = createManager({
|
|
|
308
354
|
Creating a shortcut requires a the key/commands we created and the manager options to create a valid shortcut.
|
|
309
355
|
|
|
310
356
|
```ts
|
|
311
|
-
const shortcut =createShortcut({
|
|
357
|
+
const shortcut = createShortcut({
|
|
312
358
|
command: "test",
|
|
313
359
|
chain: [["a"]],
|
|
314
|
-
condition: createCondition("a || b"
|
|
360
|
+
condition: createCondition("a || b"),
|
|
315
361
|
enabled: true,
|
|
316
362
|
}, {options, keys, commands}).unwrap()
|
|
317
363
|
|
|
@@ -379,6 +425,10 @@ Note that while the built in errors are property specific, custom errors are not
|
|
|
379
425
|
|
|
380
426
|
A helper class `ShortcutManagerManager` is provided to help manage multiple managers.
|
|
381
427
|
|
|
428
|
+
<details>
|
|
429
|
+
|
|
430
|
+
<summary>Click to Expand</summary>
|
|
431
|
+
|
|
382
432
|
```ts
|
|
383
433
|
import { ShortcutManagerManager } from "@witchcraft/spellcraft"
|
|
384
434
|
|
|
@@ -435,6 +485,9 @@ managerManager.duplicateManager("myManagerName", "myDuplicateManagerName")
|
|
|
435
485
|
managerManager.deleteManager("myManagerName")
|
|
436
486
|
managerManager.renameManager("myManagerName", "myNewManagerName")
|
|
437
487
|
```
|
|
488
|
+
|
|
489
|
+
</details>
|
|
490
|
+
|
|
438
491
|
You can then use the active manager's `onSet*Prop` hooks to call debouncedSave. You can wrap only the active manager to intercept all it's `onSet*Prop` calls. See the `useMultipleManagers` composable in the demo.
|
|
439
492
|
|
|
440
493
|
## Other Helpers and Utilities
|
|
@@ -453,6 +506,7 @@ There are many helpers provided to simplify common use cases under `/helpers`. S
|
|
|
453
506
|
There's also some smaller utility functions in `/utils`:
|
|
454
507
|
- `equals/dedupe/clone/*Key` These are particularly important for manipulating chords. This is because keys which are variants of eachother (see `Key.variants`) do not have matching ids and we usually want to be able to dedupe by the variants as well.
|
|
455
508
|
- `isAny/Trigger/Wheel/MouseKey`.
|
|
509
|
+
- `getKeyCodesFromKeys` for getting the raw key codes from a list of keys for use with third-party libraries.
|
|
456
510
|
|
|
457
511
|
|
|
458
512
|
There's also a few other functions that in the future might be moved from the demo were I created them and into the library. See [demo/src/common](https://github.com/AlansCodeLog/spellcraft/tree/master/demo/src/common).
|
|
@@ -490,18 +544,28 @@ Under gnome at least, if a key (usually Ctrl) is set to locate the cursor, it wi
|
|
|
490
544
|
|
|
491
545
|
# FAQ
|
|
492
546
|
|
|
493
|
-
|
|
547
|
+
|
|
548
|
+
<details>
|
|
549
|
+
|
|
550
|
+
<summary>Browser shortcuts interfere with certain shortcuts, how can this be avoided?</summary>
|
|
494
551
|
|
|
495
552
|
You can use a listener on the manager to e.preventDefault() some of these, but this doesn't work for all of them.
|
|
496
553
|
|
|
497
554
|
If available you can also try using the [Keyboard API's](https://developer.mozilla.org/en-US/docs/Web/API/Keyboard_API) lock method (see [Keyboard Locking](https://developer.mozilla.org/en-US/docs/Web/API/Keyboard_API#keyboard_locking) ).
|
|
498
555
|
|
|
499
|
-
|
|
556
|
+
</details>
|
|
557
|
+
|
|
558
|
+
<details>
|
|
559
|
+
|
|
560
|
+
<summary>How to label keys with their local names?</summary>
|
|
500
561
|
|
|
501
562
|
If the [Keyboard API](https://developer.mozilla.org/en-US/docs/Web/API/Keyboard_API) is available, you can use it's [navigator.keyboard.getLayoutMap method.](https://developer.mozilla.org/en-US/docs/Web/API/Keyboard/getLayoutMap). Helpers (getKeyboardLayoutMap and labelWithNavigator) are provided for this purpose, see them for details.
|
|
502
563
|
|
|
564
|
+
</details>
|
|
503
565
|
|
|
504
|
-
|
|
566
|
+
<details>
|
|
567
|
+
|
|
568
|
+
<summary>How to set multiple manager properties safely (i.e. batch replace shortcuts/commands/keys)?</summary>
|
|
505
569
|
|
|
506
570
|
This can be an issue because there isn't a way to tell the manager you want to replace *multiple* properties and it might be impossible to, for example, replace commands with a smaller subset but not have it error even if you're planning to replace the shortcuts so they don't contain missing commands.
|
|
507
571
|
|
|
@@ -519,7 +583,11 @@ if (isValidManager(manager)) {
|
|
|
519
583
|
}
|
|
520
584
|
|
|
521
585
|
```
|
|
522
|
-
|
|
586
|
+
</details>
|
|
587
|
+
|
|
588
|
+
<details>
|
|
589
|
+
|
|
590
|
+
<summary>How to create `modifier-only` shortcuts? (e.g. a shortcut `Ctrl` that changes the some state like enabling multiple selection).</summary>
|
|
523
591
|
|
|
524
592
|
To do this, instead of clearing the manager's chain, you just set the state directly.
|
|
525
593
|
|
|
@@ -563,17 +631,48 @@ function addToSelected(item) {
|
|
|
563
631
|
}
|
|
564
632
|
</script>
|
|
565
633
|
```
|
|
566
|
-
|
|
634
|
+
</details>
|
|
635
|
+
|
|
636
|
+
<details>
|
|
637
|
+
|
|
638
|
+
<summary>How to show pretty shortcut strings to the user?</summary>
|
|
639
|
+
|
|
640
|
+
To show a pretty stringified version of a shortcut, use `manager.options.stringifier.stringify(shortcut.chain, manager)`.
|
|
641
|
+
|
|
642
|
+
Here's an advanced example in vue that shows a hint only when needed:
|
|
643
|
+
|
|
644
|
+
```ts
|
|
645
|
+
const usageInstructions = computed(() => {
|
|
646
|
+
const manager = shortcuts.activeManager!.value!
|
|
647
|
+
const s = manager.options.stringifier
|
|
648
|
+
const context = shortcuts.context!
|
|
649
|
+
const isDragging = context.value.layout.drag.isDragging
|
|
650
|
+
const splitChain = manager.shortcuts.entries.find(_ => _.command === LAYOUT_COMMANDS.dragModifierSplit)?.chain
|
|
651
|
+
|
|
652
|
+
return isDragging && splitChain
|
|
653
|
+
`Hold ${s.stringify(splitChain, manager)} to Split`
|
|
654
|
+
: undefined,
|
|
655
|
+
})
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
</details>
|
|
659
|
+
|
|
660
|
+
<details>
|
|
661
|
+
|
|
662
|
+
<summary>How to type the context?</summary>
|
|
663
|
+
|
|
664
|
+
Types can be extended multiple times from different files so long as the `NAME` part below is unique for each extension.
|
|
567
665
|
|
|
568
666
|
```ts [global.ts]
|
|
569
667
|
declare module "@witchcraft/spellcraft/types" {
|
|
570
668
|
// or for nuxt
|
|
571
669
|
// declare module "#witchcraft/spellcraft/types.js" {
|
|
572
670
|
export interface Register {
|
|
573
|
-
|
|
671
|
+
ShortcutManagerContextNAME: { //replace NAME with something
|
|
574
672
|
// extend types here
|
|
575
673
|
}
|
|
576
674
|
}
|
|
577
675
|
}
|
|
578
676
|
```
|
|
677
|
+
</details>
|
|
579
678
|
|
|
@@ -4,6 +4,7 @@ import { defaultSorter } from "../defaults/KeysSorter.js";
|
|
|
4
4
|
import { defaultStringifier } from "../defaults/Stringifier.js";
|
|
5
5
|
export function createManagerOptions(rawOpts) {
|
|
6
6
|
const options = {
|
|
7
|
+
shortcutEqualityStrategy: "ignoreCommand",
|
|
7
8
|
sorter: defaultSorter,
|
|
8
9
|
stringifier: defaultStringifier,
|
|
9
10
|
conditionEquals: defaultConditionEquals,
|
|
@@ -6,7 +6,7 @@ import type { CanHookErrors, Manager, MultipleErrors, PickManager, Shortcut, Sho
|
|
|
6
6
|
* Creates a set of shortcuts.
|
|
7
7
|
*
|
|
8
8
|
*/
|
|
9
|
-
export declare function createShortcuts<THooks extends Manager["hooks"], TRawShortcuts extends Shortcut[], TCheck extends boolean | "only" = true>(shortcutsList: TRawShortcuts, manager: Pick<Manager, "keys" | "commands"> & PickManager<"options", "evaluateCondition" | "conditionEquals" | "stringifier" | "sorter"> & {
|
|
9
|
+
export declare function createShortcuts<THooks extends Manager["hooks"], TRawShortcuts extends Shortcut[], TCheck extends boolean | "only" = true>(shortcutsList: TRawShortcuts, manager: Pick<Manager, "keys" | "commands"> & PickManager<"options", "evaluateCondition" | "conditionEquals" | "stringifier" | "sorter" | "shortcutEqualityStrategy"> & {
|
|
10
10
|
hooks?: THooks;
|
|
11
11
|
}, opts?: Partial<Pick<Shortcuts, "ignoreModifierConflicts" | "ignoreChainConflicts">>, { check }?: {
|
|
12
12
|
check?: boolean | "only";
|
|
@@ -20,7 +20,7 @@ export function setShortcutsProp(prop, val, manager, {
|
|
|
20
20
|
castType(manager);
|
|
21
21
|
const shortcut = val;
|
|
22
22
|
const existing = shortcuts.entries.find(
|
|
23
|
-
(_) => equalsShortcut(shortcut, _, manager
|
|
23
|
+
(_) => equalsShortcut(shortcut, _, manager) || doesShortcutConflict(_, shortcut, manager)
|
|
24
24
|
);
|
|
25
25
|
if (existing) {
|
|
26
26
|
return Err(new KnownError(
|
|
@@ -41,7 +41,7 @@ export function setShortcutsProp(prop, val, manager, {
|
|
|
41
41
|
case "entries@remove": {
|
|
42
42
|
const shortcut = val;
|
|
43
43
|
const existing = shortcuts.entries.find(
|
|
44
|
-
(_) => _ === shortcut || equalsShortcut(shortcut, _, manager, {
|
|
44
|
+
(_) => _ === shortcut || equalsShortcut(shortcut, _, manager, { shortcutEqualityStrategy: "all" })
|
|
45
45
|
);
|
|
46
46
|
if (existing === void 0) {
|
|
47
47
|
return Err(new KnownError(
|
|
@@ -77,7 +77,7 @@ export function setShortcutsProp(prop, val, manager, {
|
|
|
77
77
|
}
|
|
78
78
|
case "entries@remove": {
|
|
79
79
|
const shortcut = val;
|
|
80
|
-
const i = shortcuts.entries.findIndex((_) => _ === shortcut || equalsShortcut(shortcut, _, manager, {
|
|
80
|
+
const i = shortcuts.entries.findIndex((_) => _ === shortcut || equalsShortcut(shortcut, _, manager, { shortcutEqualityStrategy: "all" }));
|
|
81
81
|
if (i < 0) {
|
|
82
82
|
throw new Error("If used correctly, shortcut should exist at this point, but it does not.");
|
|
83
83
|
}
|
|
@@ -1,19 +1,5 @@
|
|
|
1
1
|
import type { Condition } from "../types/index.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* The default method does a simplistic object check, and otherwise a check of the `text` property for equality. If both conditions are undefined, note this will return true.
|
|
6
|
-
*
|
|
7
|
-
* If you override the default conditionEquals option you will likely want it to just always return false.
|
|
8
|
-
*
|
|
9
|
-
* Why? Because unless you're using simple single variable conditions that you can presort to make them uniquely identifiable (i.e. not boolean expressions, e.g. `!a b !c`), this will return A LOT of false negatives.
|
|
10
|
-
*
|
|
11
|
-
* Why the false negatives? Because two conditions might be functionally equal but have differing representations (e.g: `a && b`, `b && a`). You might think, lets normalize them all, but normalizing boolean expressions (converting them to CNF) can be dangerous with very long expressions because it can take exponential time.
|
|
12
|
-
*
|
|
13
|
-
* Now the main reason for checking the equality of two conditions is to check if two shortcuts might conflict. If we're using boolean expressions it just can't be done safely.
|
|
14
|
-
*
|
|
15
|
-
* This is a personal preference, but if we have a method that gives false negatives it can be confusing that some shortcuts immediately error when added because their conditions are simple, while others don't until triggered. The simpler, more consistent alternative is to only have them error on triggering. Aditionally conflicting conditions can be shown on the keyboard layout when then user picks contexts to check against.
|
|
16
|
-
*
|
|
17
|
-
* Why use the default implementation at all then? Well, shortcuts aren't the only ones that have conditions, commands can too, but unlike shortcuts, usually it's developers who are in charge of assigning a command's condition, and since they are usually simple, it's more possible to make sure the conditions are unique (e.g. tests could enforce they're unique by converting them all to CNF and pre-checking them for equality).
|
|
3
|
+
* IMPORTANT: Please read {@link ConditionComparer} before using.
|
|
18
4
|
*/
|
|
19
|
-
export declare function defaultConditionEquals<TCondition extends Condition>(conditionA
|
|
5
|
+
export declare function defaultConditionEquals<TCondition extends Condition>(conditionA: TCondition | undefined, conditionB: Condition | undefined, _type: "shortcut" | "command"): conditionB is TCondition;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function defaultConditionEquals(conditionA, conditionB) {
|
|
1
|
+
export function defaultConditionEquals(conditionA, conditionB, _type) {
|
|
2
2
|
if (conditionA === conditionB) return true;
|
|
3
3
|
if (!conditionA || !conditionB) return false;
|
|
4
4
|
return conditionA.text === conditionB.text;
|
|
@@ -15,4 +15,4 @@ import type { Manager, PickManager, Shortcut } from "../types/index.js";
|
|
|
15
15
|
*/
|
|
16
16
|
export declare function doesShortcutConflict<TShortcut extends Shortcut>(shortcutA: TShortcut, shortcutB: Shortcut, manager: Pick<Manager, "keys" | "commands" | "shortcuts"> & {
|
|
17
17
|
context?: Manager["context"];
|
|
18
|
-
} & PickManager<"options", "evaluateCondition" | "conditionEquals">): boolean;
|
|
18
|
+
} & PickManager<"options", "evaluateCondition" | "conditionEquals" | "shortcutEqualityStrategy">): boolean;
|
|
@@ -24,7 +24,7 @@ export function doesShortcutConflict(shortcutA, shortcutB, manager) {
|
|
|
24
24
|
if (!shortcutCondition) return false;
|
|
25
25
|
}
|
|
26
26
|
} else {
|
|
27
|
-
if (!conditionEquals(shortcutA.condition, shortcutB.condition)) return false;
|
|
27
|
+
if (!conditionEquals(shortcutA.condition, shortcutB.condition, "shortcut")) return false;
|
|
28
28
|
}
|
|
29
29
|
const { keys } = manager;
|
|
30
30
|
if (shortcutA.chain.length === 0 || shortcutB.chain.length === 0) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export function equalsCommand(commandA, commandB, manager) {
|
|
2
2
|
if (commandA === commandB) return true;
|
|
3
|
-
return commandA.name === commandB.name && commandA.execute === commandB.execute && manager.options.conditionEquals(commandA.condition, commandB.condition) && commandA.description === commandB.description;
|
|
3
|
+
return commandA.name === commandB.name && commandA.execute === commandB.execute && manager.options.conditionEquals(commandA.condition, commandB.condition, "command") && commandA.description === commandB.description;
|
|
4
4
|
}
|
|
@@ -2,8 +2,10 @@ import type { Manager, PickManager, Shortcut } from "../types/index.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* Returns whether the shortcut passed is equal to shortcutA one.
|
|
4
4
|
*
|
|
5
|
-
* To return true, their keys and
|
|
5
|
+
* To return true, their keys, conditions, and commands (depends on the configured `manager.options.shortcutEqualityStrategy`) must be equal.
|
|
6
|
+
*
|
|
7
|
+
* See {@link Manager.options.shortcutEqualityStrategy} for details.
|
|
8
|
+
*
|
|
9
|
+
* You can temporarily override the strategy witht the third parameter.
|
|
6
10
|
*/
|
|
7
|
-
export declare function equalsShortcut<TShortcut extends Shortcut>(shortcutA: TShortcut, shortcutB: Shortcut, manager: Pick<Manager, "keys" | "commands"> & PickManager<"options", "evaluateCondition" | "conditionEquals"
|
|
8
|
-
ignoreCommand?: boolean;
|
|
9
|
-
}): shortcutB is TShortcut;
|
|
11
|
+
export declare function equalsShortcut<TShortcut extends Shortcut>(shortcutA: TShortcut, shortcutB: Shortcut, manager: Pick<Manager, "keys" | "commands"> & PickManager<"options", "evaluateCondition" | "conditionEquals" | "shortcutEqualityStrategy">, overrides?: PickManager<"options", "shortcutEqualityStrategy">["options"]): shortcutB is TShortcut;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { equalsCommand } from "./equalsCommand.js";
|
|
2
2
|
import { equalsKeys } from "../utils/equalsKeys.js";
|
|
3
|
-
export function equalsShortcut(shortcutA, shortcutB, manager,
|
|
3
|
+
export function equalsShortcut(shortcutA, shortcutB, manager, overrides) {
|
|
4
4
|
if (shortcutA.forceUnequal || shortcutB.forceUnequal) return false;
|
|
5
5
|
if (shortcutA === shortcutB) return true;
|
|
6
|
-
|
|
6
|
+
const commandA = shortcutA.command ? manager.commands.entries[shortcutA.command] : void 0;
|
|
7
|
+
const commandB = shortcutB.command ? manager.commands.entries[shortcutB.command] : void 0;
|
|
8
|
+
const shortcutEqualityStrategy = overrides?.shortcutEqualityStrategy ?? manager.options.shortcutEqualityStrategy;
|
|
9
|
+
return equalsKeys(shortcutA.chain, shortcutB.chain, manager.keys, void 0, { allowVariants: true }) && manager.options.conditionEquals(shortcutA.condition, shortcutB.condition, "shortcut") && (shortcutEqualityStrategy === "ignoreCommand" || shortcutEqualityStrategy === "ignoreCommandWithDifferentCondition" && commandA?.condition && commandB?.condition && manager.options.conditionEquals(commandA.condition, commandB.condition, "command") || shortcutA.command === shortcutB.command && shortcutA.command === void 0 || shortcutA.command !== void 0 && shortcutB.command !== void 0 && equalsCommand(manager.commands.entries[shortcutA.command], manager.commands.entries[shortcutB.command], manager));
|
|
7
10
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { getKeyCodesFromKeys } from "./getKeyCodesFromKeys.js";
|
|
2
|
+
import type { Keys } from "../types/keys.js";
|
|
3
|
+
/**
|
|
4
|
+
* Given an list of key ids, returns an array of deduped key codes. If you have a list of `Key`s instead use {@link getKeyCodesFromKeys}.
|
|
5
|
+
*
|
|
6
|
+
* Useful for when the codes must be passed to a third-party library (for example, a dragging library that takes a list modifiers).
|
|
7
|
+
*
|
|
8
|
+
* This properly takes a look at the key and all it's variants.
|
|
9
|
+
*
|
|
10
|
+
* `skipIdIfHasVariants` is true by default and will skip adding the key's id if it has variants as it's assumed the id is not a valid name.
|
|
11
|
+
*/
|
|
12
|
+
export declare function getKeyCodesFromKeyIds(keyIds: string[], keys: Keys, opts: Parameters<typeof getKeyCodesFromKeys>[1]): string[];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { getKeyCodesFromKeys } from "./getKeyCodesFromKeys.js";
|
|
2
|
+
import { getKeyFromIdOrVariant } from "./getKeyFromIdOrVariant.js";
|
|
3
|
+
export function getKeyCodesFromKeyIds(keyIds, keys, opts) {
|
|
4
|
+
const keysList = keyIds.flatMap((id) => getKeyFromIdOrVariant(id, keys).unwrap());
|
|
5
|
+
return getKeyCodesFromKeys(keysList, opts);
|
|
6
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Given an list of keys, returns an array of deduped key codes.
|
|
3
|
+
*
|
|
4
|
+
* Useful for when the codes must be passed to a third-party library (for example, a dragging library that takes a list modifiers).
|
|
5
|
+
*
|
|
6
|
+
* This properly takes a look at the key and all it's variants.
|
|
7
|
+
*
|
|
8
|
+
* `skipIdIfHasVariants` is true by default and will skip adding the key's id if it has variants as it's assumed the id is not a valid name.
|
|
9
|
+
*/
|
|
10
|
+
import type { Key } from "../types/keys.js";
|
|
11
|
+
export declare function getKeyCodesFromKeys(keys: Key[], { skipIdIfHasVariants }?: {
|
|
12
|
+
skipIdIfHasVariants?: boolean;
|
|
13
|
+
}): string[];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function getKeyCodesFromKeys(keys, { skipIdIfHasVariants = true } = {}) {
|
|
2
|
+
const res = /* @__PURE__ */ new Set();
|
|
3
|
+
for (const key of keys) {
|
|
4
|
+
if (key.variants && key.variants.length > 0) {
|
|
5
|
+
if (!skipIdIfHasVariants) {
|
|
6
|
+
res.add(key.id);
|
|
7
|
+
}
|
|
8
|
+
for (const variant of key.variants) {
|
|
9
|
+
res.add(variant);
|
|
10
|
+
}
|
|
11
|
+
} else {
|
|
12
|
+
res.add(key.id);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return Array.from(res);
|
|
16
|
+
}
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createResolver, defineNuxtModule, addTypeTemplate, addImportsDir } from '@nuxt/kit';
|
|
2
2
|
|
|
3
3
|
const { resolve } = createResolver(import.meta.url);
|
|
4
|
-
const module = defineNuxtModule({
|
|
4
|
+
const module$1 = defineNuxtModule({
|
|
5
5
|
meta: {
|
|
6
6
|
name: "witchcraftSpellcraft",
|
|
7
7
|
configKey: "witchcraftSpellcraft"
|
|
@@ -32,4 +32,4 @@ const module = defineNuxtModule({
|
|
|
32
32
|
}
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
export { module as default };
|
|
35
|
+
export { module$1 as default };
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
import type { OrToAnd } from "@alanscodelog/utils/types";
|
|
1
2
|
export interface Register {
|
|
2
3
|
}
|
|
3
|
-
type
|
|
4
|
-
|
|
5
|
-
} ? T : unknown;
|
|
6
|
-
export type ContextInfo = ExtendedContextInfo & {
|
|
4
|
+
export type ExtendedShortcutManagerInfo = keyof Register extends `ShortcutManagerContext${infer T}` ? Register extends Record<`ShortcutManagerContext${T}`, infer U> ? U : {} : {};
|
|
5
|
+
export type ContextInfo = OrToAnd<ExtendedShortcutManagerInfo> & {
|
|
7
6
|
count: Record<string, number>;
|
|
8
7
|
isActive: Record<string, boolean>;
|
|
9
8
|
};
|
|
@@ -24,6 +24,8 @@ export interface Condition {
|
|
|
24
24
|
* This is a personal preference, but if we have a method that gives false negatives it can be confusing that some shortcuts immediately error when added because their conditions are simple, while others don't until triggered. The simpler, more consistent alternative is to only have them error on triggering. Aditionally conflicting conditions can be shown on the keyboard layout when then user picks contexts to check against.
|
|
25
25
|
*
|
|
26
26
|
* Why use the default implementation at all then? Well, shortcuts aren't the only ones that have conditions, commands can too, but unlike shortcuts, usually it's developers who are in charge of assigning a command's condition, and since they are usually simple, it's more possible to make sure the conditions are unique (e.g. tests could enforce they're unique by converting them all to CNF and pre-checking them for equality).
|
|
27
|
+
*
|
|
28
|
+
* This is why the manager now has the option `shortcutEqualityStrategy` which can be se to `ignoreCommandWithDifferentCondition`. You can implement a comparer that only checks when a command is being checked.
|
|
27
29
|
*/
|
|
28
|
-
export type ConditionComparer = (conditionA
|
|
30
|
+
export type ConditionComparer = (conditionA: Condition | undefined, conditionB: Condition | undefined, type: "shortcut" | "command") => boolean;
|
|
29
31
|
export type ConditionEvaluator<TContext extends Context<any>> = (condition: Condition, context: TContext) => boolean;
|
package/dist/types/manager.d.ts
CHANGED
|
@@ -114,9 +114,20 @@ export type Manager<THooks extends Partial<Hooks> = Partial<Hooks>, TKeys extend
|
|
|
114
114
|
/**
|
|
115
115
|
* Determines if two conditions are equal.
|
|
116
116
|
*
|
|
117
|
-
* This is actually not a good idea to implement if you use boolean conditions. See {@link ConditionComparer} for why.
|
|
117
|
+
* This is actually not a good idea to implement for shortcuts if you use boolean conditions. See {@link ConditionComparer} for why.
|
|
118
|
+
*
|
|
119
|
+
* See also {@link Manager.options.shortcutEqualityStrategy}.
|
|
118
120
|
*/
|
|
119
121
|
conditionEquals: ConditionComparer;
|
|
122
|
+
/**
|
|
123
|
+
* Determines how shortcuts are compared.
|
|
124
|
+
*
|
|
125
|
+
* - `ignoreCommand` (default) will ignore both shortcuts' commands. If everything else is equal, they are considered equal.
|
|
126
|
+
* - `ignoreCommandWithDifferentCondition` will ignore both commands' names and only compare their conditions. If everything else is equal, they are considered equal. Use it if you allow command conditions to equal eachother. See {@link ConditionComparer} for details.
|
|
127
|
+
* - `all` will compare everything.
|
|
128
|
+
*
|
|
129
|
+
*/
|
|
130
|
+
shortcutEqualityStrategy: "ignoreCommand" | "ignoreCommandWithDifferentCondition" | "all";
|
|
120
131
|
/** Determines how conditions are evaluated. */
|
|
121
132
|
evaluateCondition: ConditionEvaluator<TContext>;
|
|
122
133
|
/** Enable/disable triggering of shortcuts. The manager otherwise works as normal. */
|
|
@@ -126,12 +126,12 @@ export type CanHookShortcutProps = Exclude<OnHookShortcutProps, "forceUnequal">;
|
|
|
126
126
|
export type SyntheticOnHookShortcutsProps = "entries@add" | "entries@remove";
|
|
127
127
|
export type CanHookShortcutsProps = SyntheticOnHookShortcutsProps;
|
|
128
128
|
export type OnHookShortcutsProps = SyntheticOnHookShortcutsProps;
|
|
129
|
-
type BaseShortcutsManager = Pick<Manager, "keys" | "commands" | "shortcuts"> & PickManager<"options", "evaluateCondition" | "conditionEquals" | "stringifier"> & Record<any, any>;
|
|
129
|
+
type BaseShortcutsManager = Pick<Manager, "keys" | "commands" | "shortcuts"> & PickManager<"options", "evaluateCondition" | "conditionEquals" | "stringifier" | "shortcutEqualityStrategy"> & Record<any, any>;
|
|
130
130
|
export type ShortcutsSetEntries = {
|
|
131
131
|
"entries@add": {
|
|
132
132
|
val: Shortcut;
|
|
133
133
|
hooks: GetShortcutHooks<`entries@add`>;
|
|
134
|
-
manager: BaseShortcutsManager & PickManager<"options", "sorter">;
|
|
134
|
+
manager: BaseShortcutsManager & PickManager<"options", "sorter" | "shortcutEqualityStrategy">;
|
|
135
135
|
error: typeof SHORTCUT_ERROR.DUPLICATE_SHORTCUT | typeof SHORTCUT_ERROR.UNKNOWN_COMMAND | ChainError;
|
|
136
136
|
};
|
|
137
137
|
"entries@remove": {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@witchcraft/spellcraft",
|
|
3
3
|
"description": "A shortcut manager library for handling ALL the shortcut needs of an application.",
|
|
4
|
-
"version": "0.0
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/core/index.js",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@witchcraft/expressit": "^0.4.
|
|
43
|
-
"@witchcraft/ui": "^0.3.
|
|
42
|
+
"@witchcraft/expressit": "^0.4.2",
|
|
43
|
+
"@witchcraft/ui": "^0.3.20"
|
|
44
44
|
},
|
|
45
45
|
"peerDependenciesMeta": {
|
|
46
46
|
"@witchcraft/ui": {
|
|
@@ -52,45 +52,46 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@alanscodelog/utils": "^6.0.2",
|
|
55
|
-
"@witchcraft/expressit": "^0.4.
|
|
56
|
-
"@witchcraft/ui": "^0.3.
|
|
57
|
-
"
|
|
55
|
+
"@witchcraft/expressit": "^0.4.2",
|
|
56
|
+
"@witchcraft/ui": "^0.3.20",
|
|
57
|
+
"reka-ui": "^2.8.0",
|
|
58
|
+
"vue": "^3.5.27"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
60
61
|
"@alanscodelog/commitlint-config": "^3.1.2",
|
|
61
62
|
"@alanscodelog/eslint-config": "^6.3.1",
|
|
62
63
|
"@alanscodelog/semantic-release-config": "^6.0.0",
|
|
63
64
|
"@alanscodelog/tsconfigs": "^6.2.0",
|
|
64
|
-
"@alanscodelog/vite-config": "^0.0.
|
|
65
|
-
"@commitlint/cli": "^
|
|
66
|
-
"@iconify/json": "^2.2.
|
|
67
|
-
"@nuxt/eslint-config": "^1.
|
|
68
|
-
"@nuxt/kit": "^4.
|
|
65
|
+
"@alanscodelog/vite-config": "^0.0.7",
|
|
66
|
+
"@commitlint/cli": "^20.4.1",
|
|
67
|
+
"@iconify/json": "^2.2.436",
|
|
68
|
+
"@nuxt/eslint-config": "^1.13.0",
|
|
69
|
+
"@nuxt/kit": "^4.3.0",
|
|
69
70
|
"@nuxt/module-builder": "^1.0.2",
|
|
70
|
-
"@nuxt/schema": "^4.
|
|
71
|
+
"@nuxt/schema": "^4.3.0",
|
|
71
72
|
"@nuxt/types": "^2.18.1",
|
|
72
|
-
"@types/node": "^
|
|
73
|
-
"@vitejs/plugin-vue": "^6.0.
|
|
73
|
+
"@types/node": "^25.2.1",
|
|
74
|
+
"@vitejs/plugin-vue": "^6.0.4",
|
|
74
75
|
"@vitest/coverage-c8": "^0.33.0",
|
|
75
76
|
"concurrently": "^9.2.1",
|
|
76
|
-
"cross-env": "^10.
|
|
77
|
+
"cross-env": "^10.1.0",
|
|
77
78
|
"defu": "^6.1.4",
|
|
78
79
|
"fast-glob": "^3.3.3",
|
|
79
80
|
"http-server": "^14.1.1",
|
|
80
81
|
"husky": "^9.1.7",
|
|
81
82
|
"indexit": "2.1.0-beta.3",
|
|
82
83
|
"madge": "^8.0.0",
|
|
83
|
-
"nuxt": "^4.
|
|
84
|
+
"nuxt": "^4.3.0",
|
|
84
85
|
"onchange": "^7.1.0",
|
|
85
|
-
"semantic-release": "^
|
|
86
|
-
"tailwindcss": "^4.1.
|
|
87
|
-
"typedoc": "0.28.
|
|
88
|
-
"typescript": "^5.9.
|
|
86
|
+
"semantic-release": "^25.0.3",
|
|
87
|
+
"tailwindcss": "^4.1.18",
|
|
88
|
+
"typedoc": "0.28.16",
|
|
89
|
+
"typescript": "^5.9.3",
|
|
89
90
|
"unbuild": "^3.6.1",
|
|
90
|
-
"unplugin-icons": "^
|
|
91
|
-
"vite-tsconfig-paths": "^
|
|
92
|
-
"vitest": "^
|
|
93
|
-
"vue-tsc": "^3.
|
|
91
|
+
"unplugin-icons": "^23.0.1",
|
|
92
|
+
"vite-tsconfig-paths": "^6.0.5",
|
|
93
|
+
"vitest": "^4.0.18",
|
|
94
|
+
"vue-tsc": "^3.2.4"
|
|
94
95
|
},
|
|
95
96
|
"author": "Alan <alanscodelog@gmail.com>",
|
|
96
97
|
"repository": "https://github.com/witchcraftjs/spellcraft",
|
|
@@ -126,7 +127,8 @@
|
|
|
126
127
|
"node": ">=20.0.0"
|
|
127
128
|
},
|
|
128
129
|
"publishConfig": {
|
|
129
|
-
"access": "public"
|
|
130
|
+
"access": "public",
|
|
131
|
+
"provenance": true
|
|
130
132
|
},
|
|
131
133
|
"scripts": {
|
|
132
134
|
"build": "nuxt-module-build prepare && nuxt-module-build build && nuxi generate playground",
|
|
@@ -16,6 +16,7 @@ export function createManagerOptions(
|
|
|
16
16
|
rawOpts: Parameters<typeof createManager>[0]["options"]
|
|
17
17
|
): Manager["options"] {
|
|
18
18
|
const options: Manager["options"] = {
|
|
19
|
+
shortcutEqualityStrategy: "ignoreCommand",
|
|
19
20
|
sorter: defaultSorter,
|
|
20
21
|
stringifier: defaultStringifier,
|
|
21
22
|
conditionEquals: defaultConditionEquals,
|
|
@@ -17,7 +17,7 @@ export function createShortcuts<
|
|
|
17
17
|
>(
|
|
18
18
|
shortcutsList: TRawShortcuts,
|
|
19
19
|
manager: Pick<Manager, "keys" | "commands">
|
|
20
|
-
& PickManager<"options", | "evaluateCondition" | "conditionEquals" | "stringifier" | "sorter">
|
|
20
|
+
& PickManager<"options", | "evaluateCondition" | "conditionEquals" | "stringifier" | "sorter" | "shortcutEqualityStrategy">
|
|
21
21
|
& { hooks?: THooks },
|
|
22
22
|
opts?: Partial<Pick<Shortcuts, "ignoreModifierConflicts" | "ignoreChainConflicts">>,
|
|
23
23
|
{
|
|
@@ -43,7 +43,7 @@ export function setShortcutsProp<
|
|
|
43
43
|
|
|
44
44
|
const shortcut = val as any as Shortcut
|
|
45
45
|
const existing = (shortcuts.entries).find(_ =>
|
|
46
|
-
equalsShortcut(shortcut, _, manager
|
|
46
|
+
equalsShortcut(shortcut, _, manager)
|
|
47
47
|
|| doesShortcutConflict(_, shortcut, manager)
|
|
48
48
|
)
|
|
49
49
|
|
|
@@ -67,7 +67,7 @@ export function setShortcutsProp<
|
|
|
67
67
|
const shortcut = val as any as Shortcut
|
|
68
68
|
// note we don't ignore the command here, we want to find an exact match
|
|
69
69
|
const existing = (shortcuts.entries).find(_ =>
|
|
70
|
-
_ === shortcut || equalsShortcut(shortcut, _, manager, {
|
|
70
|
+
_ === shortcut || equalsShortcut(shortcut, _, manager, { shortcutEqualityStrategy: "all" })
|
|
71
71
|
)
|
|
72
72
|
if (existing === undefined) {
|
|
73
73
|
return Err(new KnownError(
|
|
@@ -105,7 +105,7 @@ export function setShortcutsProp<
|
|
|
105
105
|
}
|
|
106
106
|
case "entries@remove": {
|
|
107
107
|
const shortcut = val
|
|
108
|
-
const i = shortcuts.entries.findIndex(_ => _ === shortcut || equalsShortcut(shortcut, _, manager, {
|
|
108
|
+
const i = shortcuts.entries.findIndex(_ => _ === shortcut || equalsShortcut(shortcut, _, manager, { shortcutEqualityStrategy: "all" }))
|
|
109
109
|
if (i < 0) {
|
|
110
110
|
throw new Error("If used correctly, shortcut should exist at this point, but it does not.")
|
|
111
111
|
}
|
|
@@ -1,25 +1,12 @@
|
|
|
1
1
|
import type { Condition } from "../types/index.js"
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* The default method does a simplistic object check, and otherwise a check of the `text` property for equality. If both conditions are undefined, note this will return true.
|
|
7
|
-
*
|
|
8
|
-
* If you override the default conditionEquals option you will likely want it to just always return false.
|
|
9
|
-
*
|
|
10
|
-
* Why? Because unless you're using simple single variable conditions that you can presort to make them uniquely identifiable (i.e. not boolean expressions, e.g. `!a b !c`), this will return A LOT of false negatives.
|
|
11
|
-
*
|
|
12
|
-
* Why the false negatives? Because two conditions might be functionally equal but have differing representations (e.g: `a && b`, `b && a`). You might think, lets normalize them all, but normalizing boolean expressions (converting them to CNF) can be dangerous with very long expressions because it can take exponential time.
|
|
13
|
-
*
|
|
14
|
-
* Now the main reason for checking the equality of two conditions is to check if two shortcuts might conflict. If we're using boolean expressions it just can't be done safely.
|
|
15
|
-
*
|
|
16
|
-
* This is a personal preference, but if we have a method that gives false negatives it can be confusing that some shortcuts immediately error when added because their conditions are simple, while others don't until triggered. The simpler, more consistent alternative is to only have them error on triggering. Aditionally conflicting conditions can be shown on the keyboard layout when then user picks contexts to check against.
|
|
17
|
-
*
|
|
18
|
-
* Why use the default implementation at all then? Well, shortcuts aren't the only ones that have conditions, commands can too, but unlike shortcuts, usually it's developers who are in charge of assigning a command's condition, and since they are usually simple, it's more possible to make sure the conditions are unique (e.g. tests could enforce they're unique by converting them all to CNF and pre-checking them for equality).
|
|
4
|
+
* IMPORTANT: Please read {@link ConditionComparer} before using.
|
|
19
5
|
*/
|
|
20
6
|
export function defaultConditionEquals<TCondition extends Condition>(
|
|
21
|
-
conditionA
|
|
22
|
-
conditionB
|
|
7
|
+
conditionA: TCondition | undefined,
|
|
8
|
+
conditionB: Condition | undefined,
|
|
9
|
+
_type: "shortcut" | "command"
|
|
23
10
|
): conditionB is TCondition {
|
|
24
11
|
// both are the same object or both undefined
|
|
25
12
|
if (conditionA === conditionB) return true
|
|
@@ -23,7 +23,7 @@ export function doesShortcutConflict<TShortcut extends Shortcut>(
|
|
|
23
23
|
shortcutA: TShortcut,
|
|
24
24
|
shortcutB: Shortcut,
|
|
25
25
|
manager: Pick<Manager, "keys" | "commands" | "shortcuts"> & { context?: Manager["context"] }
|
|
26
|
-
& PickManager<"options", "evaluateCondition" | "conditionEquals">
|
|
26
|
+
& PickManager<"options", "evaluateCondition" | "conditionEquals" | "shortcutEqualityStrategy">
|
|
27
27
|
): boolean {
|
|
28
28
|
const context = manager.context
|
|
29
29
|
if (!context && manager.shortcuts.useContextInConflictCheck) {
|
|
@@ -47,7 +47,7 @@ export function doesShortcutConflict<TShortcut extends Shortcut>(
|
|
|
47
47
|
if (!shortcutCondition) return false
|
|
48
48
|
}
|
|
49
49
|
} else {
|
|
50
|
-
if (!conditionEquals(shortcutA.condition, shortcutB.condition)) return false
|
|
50
|
+
if (!conditionEquals(shortcutA.condition, shortcutB.condition, "shortcut")) return false
|
|
51
51
|
}
|
|
52
52
|
const { keys } = manager
|
|
53
53
|
// an empty chain is always in conflict ?
|
|
@@ -13,6 +13,6 @@ export function equalsCommand<TCommand extends Command>(
|
|
|
13
13
|
if (commandA === commandB) return true
|
|
14
14
|
return commandA.name === commandB.name
|
|
15
15
|
&& commandA.execute === commandB.execute
|
|
16
|
-
&& manager.options.conditionEquals(commandA.condition, commandB.condition)
|
|
16
|
+
&& manager.options.conditionEquals(commandA.condition, commandB.condition, "command")
|
|
17
17
|
&& commandA.description === commandB.description
|
|
18
18
|
}
|
|
@@ -6,20 +6,32 @@ import { equalsKeys } from "../utils/equalsKeys.js"
|
|
|
6
6
|
/**
|
|
7
7
|
* Returns whether the shortcut passed is equal to shortcutA one.
|
|
8
8
|
*
|
|
9
|
-
* To return true, their keys and
|
|
9
|
+
* To return true, their keys, conditions, and commands (depends on the configured `manager.options.shortcutEqualityStrategy`) must be equal.
|
|
10
|
+
*
|
|
11
|
+
* See {@link Manager.options.shortcutEqualityStrategy} for details.
|
|
12
|
+
*
|
|
13
|
+
* You can temporarily override the strategy witht the third parameter.
|
|
10
14
|
*/
|
|
11
15
|
export function equalsShortcut<TShortcut extends Shortcut>(
|
|
12
16
|
shortcutA: TShortcut,
|
|
13
17
|
shortcutB: Shortcut,
|
|
14
|
-
manager: Pick<Manager, "keys" | "commands"> & PickManager<"options", | "evaluateCondition" | "conditionEquals">,
|
|
15
|
-
|
|
18
|
+
manager: Pick<Manager, "keys" | "commands"> & PickManager<"options", | "evaluateCondition" | "conditionEquals" | "shortcutEqualityStrategy">,
|
|
19
|
+
overrides?: PickManager<"options", "shortcutEqualityStrategy">["options"]
|
|
16
20
|
): shortcutB is TShortcut {
|
|
17
21
|
if (shortcutA.forceUnequal || shortcutB.forceUnequal) return false
|
|
18
22
|
if (shortcutA === shortcutB) return true
|
|
23
|
+
const commandA = shortcutA.command ? manager.commands.entries[shortcutA.command] : undefined
|
|
24
|
+
const commandB = shortcutB.command ? manager.commands.entries[shortcutB.command] : undefined
|
|
25
|
+
const shortcutEqualityStrategy = overrides?.shortcutEqualityStrategy ?? manager.options.shortcutEqualityStrategy
|
|
19
26
|
return (
|
|
20
27
|
equalsKeys(shortcutA.chain, shortcutB.chain, manager.keys, undefined, { allowVariants: true })
|
|
21
|
-
&& manager.options.conditionEquals(shortcutA.condition, shortcutB.condition)
|
|
22
|
-
&& (ignoreCommand
|
|
28
|
+
&& manager.options.conditionEquals(shortcutA.condition, shortcutB.condition, "shortcut")
|
|
29
|
+
&& (shortcutEqualityStrategy === "ignoreCommand"
|
|
30
|
+
|| (
|
|
31
|
+
shortcutEqualityStrategy === "ignoreCommandWithDifferentCondition"
|
|
32
|
+
&& commandA?.condition && commandB?.condition
|
|
33
|
+
&& manager.options.conditionEquals(commandA.condition, commandB.condition, "command")
|
|
34
|
+
)
|
|
23
35
|
|| (
|
|
24
36
|
shortcutA.command === shortcutB.command
|
|
25
37
|
&& shortcutA.command === undefined
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getKeyCodesFromKeys } from "./getKeyCodesFromKeys.js"
|
|
2
|
+
import { getKeyFromIdOrVariant } from "./getKeyFromIdOrVariant.js"
|
|
3
|
+
|
|
4
|
+
import type { Keys } from "../types/keys.js"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Given an list of key ids, returns an array of deduped key codes. If you have a list of `Key`s instead use {@link getKeyCodesFromKeys}.
|
|
8
|
+
*
|
|
9
|
+
* Useful for when the codes must be passed to a third-party library (for example, a dragging library that takes a list modifiers).
|
|
10
|
+
*
|
|
11
|
+
* This properly takes a look at the key and all it's variants.
|
|
12
|
+
*
|
|
13
|
+
* `skipIdIfHasVariants` is true by default and will skip adding the key's id if it has variants as it's assumed the id is not a valid name.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
export function getKeyCodesFromKeyIds(keyIds: string[], keys: Keys, opts: Parameters<typeof getKeyCodesFromKeys>[1]): string[] {
|
|
18
|
+
const keysList = keyIds.flatMap(id => getKeyFromIdOrVariant(id, keys).unwrap())
|
|
19
|
+
return getKeyCodesFromKeys(keysList, opts)
|
|
20
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Given an list of keys, returns an array of deduped key codes.
|
|
3
|
+
*
|
|
4
|
+
* Useful for when the codes must be passed to a third-party library (for example, a dragging library that takes a list modifiers).
|
|
5
|
+
*
|
|
6
|
+
* This properly takes a look at the key and all it's variants.
|
|
7
|
+
*
|
|
8
|
+
* `skipIdIfHasVariants` is true by default and will skip adding the key's id if it has variants as it's assumed the id is not a valid name.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Key } from "../types/keys.js"
|
|
12
|
+
|
|
13
|
+
export function getKeyCodesFromKeys(keys: Key[], { skipIdIfHasVariants = true}: { skipIdIfHasVariants?: boolean } = {}): string[] {
|
|
14
|
+
const res = new Set<string>()
|
|
15
|
+
for (const key of keys) {
|
|
16
|
+
if (key.variants && key.variants.length > 0) {
|
|
17
|
+
if (!skipIdIfHasVariants) {
|
|
18
|
+
res.add(key.id)
|
|
19
|
+
}
|
|
20
|
+
for (const variant of key.variants) {
|
|
21
|
+
res.add(variant)
|
|
22
|
+
}
|
|
23
|
+
} else {
|
|
24
|
+
res.add(key.id)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return Array.from(res)
|
|
28
|
+
}
|
package/src/runtime/types.ts
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
import type { OrToAnd } from "@alanscodelog/utils/types"
|
|
2
|
+
// allows users to register multiple contexts
|
|
3
|
+
// @eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
4
|
+
export interface Register {}
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
|
|
6
|
+
export type ExtendedShortcutManagerInfo = keyof Register extends `ShortcutManagerContext${infer T}`
|
|
7
|
+
? Register extends Record<`ShortcutManagerContext${T}`, infer U>
|
|
8
|
+
? U
|
|
9
|
+
// we can further constrain the type users can register if needed
|
|
10
|
+
// ? U extends { }
|
|
11
|
+
// ? U
|
|
12
|
+
// : {}
|
|
13
|
+
// : {}
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
15
|
+
: {}
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
17
|
+
: {}
|
|
5
18
|
|
|
6
|
-
|
|
19
|
+
|
|
20
|
+
export type ContextInfo = OrToAnd<ExtendedShortcutManagerInfo> & {
|
|
7
21
|
count: Record<string, number>
|
|
8
22
|
isActive: Record<string, boolean>
|
|
9
23
|
}
|
package/src/types/condition.ts
CHANGED
|
@@ -28,8 +28,10 @@ export interface Condition {
|
|
|
28
28
|
* This is a personal preference, but if we have a method that gives false negatives it can be confusing that some shortcuts immediately error when added because their conditions are simple, while others don't until triggered. The simpler, more consistent alternative is to only have them error on triggering. Aditionally conflicting conditions can be shown on the keyboard layout when then user picks contexts to check against.
|
|
29
29
|
*
|
|
30
30
|
* Why use the default implementation at all then? Well, shortcuts aren't the only ones that have conditions, commands can too, but unlike shortcuts, usually it's developers who are in charge of assigning a command's condition, and since they are usually simple, it's more possible to make sure the conditions are unique (e.g. tests could enforce they're unique by converting them all to CNF and pre-checking them for equality).
|
|
31
|
+
*
|
|
32
|
+
* This is why the manager now has the option `shortcutEqualityStrategy` which can be se to `ignoreCommandWithDifferentCondition`. You can implement a comparer that only checks when a command is being checked.
|
|
31
33
|
*/
|
|
32
|
-
export type ConditionComparer = (conditionA
|
|
34
|
+
export type ConditionComparer = (conditionA: Condition | undefined, conditionB: Condition | undefined, type: "shortcut" | "command") => boolean
|
|
33
35
|
|
|
34
36
|
export type ConditionEvaluator<TContext extends Context<any>> = (condition: Condition, context: TContext) => boolean
|
|
35
37
|
|
package/src/types/manager.ts
CHANGED
|
@@ -212,9 +212,21 @@ export type Manager<
|
|
|
212
212
|
/**
|
|
213
213
|
* Determines if two conditions are equal.
|
|
214
214
|
*
|
|
215
|
-
* This is actually not a good idea to implement if you use boolean conditions. See {@link ConditionComparer} for why.
|
|
215
|
+
* This is actually not a good idea to implement for shortcuts if you use boolean conditions. See {@link ConditionComparer} for why.
|
|
216
|
+
*
|
|
217
|
+
* See also {@link Manager.options.shortcutEqualityStrategy}.
|
|
216
218
|
*/
|
|
217
219
|
conditionEquals: ConditionComparer
|
|
220
|
+
/**
|
|
221
|
+
* Determines how shortcuts are compared.
|
|
222
|
+
*
|
|
223
|
+
* - `ignoreCommand` (default) will ignore both shortcuts' commands. If everything else is equal, they are considered equal.
|
|
224
|
+
* - `ignoreCommandWithDifferentCondition` will ignore both commands' names and only compare their conditions. If everything else is equal, they are considered equal. Use it if you allow command conditions to equal eachother. See {@link ConditionComparer} for details.
|
|
225
|
+
* - `all` will compare everything.
|
|
226
|
+
*
|
|
227
|
+
*/
|
|
228
|
+
shortcutEqualityStrategy: "ignoreCommand" | "ignoreCommandWithDifferentCondition" | "all"
|
|
229
|
+
|
|
218
230
|
/** Determines how conditions are evaluated. */
|
|
219
231
|
evaluateCondition: ConditionEvaluator<TContext>
|
|
220
232
|
/** Enable/disable triggering of shortcuts. The manager otherwise works as normal. */
|
package/src/types/shortcuts.ts
CHANGED
|
@@ -178,7 +178,7 @@ export type OnHookShortcutsProps = SyntheticOnHookShortcutsProps
|
|
|
178
178
|
|
|
179
179
|
type BaseShortcutsManager
|
|
180
180
|
= & Pick<Manager, "keys" | "commands" | "shortcuts">
|
|
181
|
-
& PickManager<"options", | "evaluateCondition" | "conditionEquals" | "stringifier">
|
|
181
|
+
& PickManager<"options", | "evaluateCondition" | "conditionEquals" | "stringifier" | "shortcutEqualityStrategy">
|
|
182
182
|
& Record<any, any>
|
|
183
183
|
|
|
184
184
|
export type ShortcutsSetEntries = {
|
|
@@ -187,7 +187,7 @@ export type ShortcutsSetEntries = {
|
|
|
187
187
|
val: Shortcut
|
|
188
188
|
hooks: GetShortcutHooks<`entries@add`>
|
|
189
189
|
manager: BaseShortcutsManager
|
|
190
|
-
& PickManager<"options", "sorter">
|
|
190
|
+
& PickManager<"options", "sorter" | "shortcutEqualityStrategy">
|
|
191
191
|
|
|
192
192
|
error:
|
|
193
193
|
| typeof SHORTCUT_ERROR.DUPLICATE_SHORTCUT
|