diff --git a/HotKeys2.Test/HotKeysContextTest.cs b/HotKeys2.Test/HotKeysContextTest.cs new file mode 100644 index 0000000..ddb1530 --- /dev/null +++ b/HotKeys2.Test/HotKeysContextTest.cs @@ -0,0 +1,143 @@ +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.JSInterop; + +namespace Toolbelt.Blazor.HotKeys2.Test; + +public class HotKeysContextTest +{ + [Test] + public void Remove_by_Key_Test() + { + // Given + using var hotkeysContext = new HotKeysContext(Task.FromResult(default(IJSObjectReference)!), NullLogger.Instance) + .Add(Code.A, () => { }) + .Add(Key.F1, () => { }, description: "Show the help document.") // This entry should be removed even though the description is unmatched. + .Add(ModCode.Shift, Code.A, () => { }) + .Add(ModKey.Ctrl | ModKey.Alt, Key.F1, () => { }); + + // When + hotkeysContext.Remove(Key.F1); + + // Then + hotkeysContext.Keys + .Select(hotkey => string.Join("+", hotkey.ToStringKeys())) + .Is("A", "Shift+A", "Ctrl+Alt+F1"); + } + + [Test] + public void Remove_by_Code_and_Mod_Test() + { + // Given + using var hotkeysContext = new HotKeysContext(Task.FromResult(default(IJSObjectReference)!), NullLogger.Instance) + .Add(Code.A, () => { }) + .Add(Key.F1, () => { }) + .Add(ModCode.Shift, Code.A, () => { }, exclude: Exclude.None) // This entry should be removed even though the exclude flag is unmatched. + .Add(ModKey.Ctrl | ModKey.Alt, Key.F1, () => { }); + + // When + hotkeysContext.Remove(ModCode.Shift, Code.A); + + // Then + hotkeysContext.Keys + .Select(hotkey => string.Join("+", hotkey.ToStringKeys())) + .Is("A", "F1", "Ctrl+Alt+F1"); + } + + [Test] + public void Remove_by_Key_and_Exclude_Test() + { + // Given + using var hotkeysContext = new HotKeysContext(Task.FromResult(default(IJSObjectReference)!), NullLogger.Instance) + .Add(Code.A, () => { }) + .Add(ModKey.Meta, Key.F1, () => { }, new() { Exclude = Exclude.ContentEditable }) + .Add(ModCode.Shift, Code.A, () => { }) + .Add(ModKey.Meta, Key.F1, () => { }); + + // When + hotkeysContext.Remove(ModKey.Meta, Key.F1, exclude: Exclude.ContentEditable); + + // Then + hotkeysContext.Keys + .Select(hotkey => string.Join("+", hotkey.ToStringKeys())) + .Is("A", "Shift+A", "Meta+F1"); + } + + [Test] + public void Remove_by_Code_and_ExcludeSelector_Test() + { + // Given + using var hotkeysContext = new HotKeysContext(Task.FromResult(default(IJSObjectReference)!), NullLogger.Instance) + .Add(Code.A, () => { }) + .Add(ModKey.Meta, Key.F1, () => { }) + .Add(Code.A, () => { }, new() { ExcludeSelector = "[data-no-hotkeys]" }) + .Add(ModKey.Meta, Key.F1, () => { }); + + // When + hotkeysContext.Remove(Code.A, excludeSelector: "[data-no-hotkeys]"); + + // Then + hotkeysContext.Keys + .Select(hotkey => string.Join("+", hotkey.ToStringKeys())) + .Is("A", "Meta+F1", "Meta+F1"); + } + + [Test] + public void Remove_by_Key_but_Ambiguous_Exception_Test() + { + // Given + using var hotkeysContext = new HotKeysContext(Task.FromResult(default(IJSObjectReference)!), NullLogger.Instance) + .Add(ModCode.Shift, Code.A, () => { }) + .Add(Key.F1, () => { }, exclude: Exclude.ContentEditable) + .Add(ModCode.Shift, Code.A, () => { }) + .Add(Key.F1, () => { }, exclude: Exclude.InputNonText); + + // When + Assert.Throws(() => hotkeysContext.Remove(Key.F1)); + + // Then + hotkeysContext.Keys + .Select(hotkey => string.Join("+", hotkey.ToStringKeys())) + .Is("Shift+A", "F1", "Shift+A", "F1"); + } + + [Test] + public void Remove_by_Code_but_Ambiguous_Exception_Test() + { + // Given + using var hotkeysContext = new HotKeysContext(Task.FromResult(default(IJSObjectReference)!), NullLogger.Instance) + .Add(ModCode.Shift, Code.A, () => { }, exclude: Exclude.ContentEditable) + .Add(Key.F1, () => { }) + .Add(ModCode.Shift, Code.A, () => { }, exclude: Exclude.InputNonText) + .Add(Key.F1, () => { }); + + // When + Assert.Throws(() => hotkeysContext.Remove(ModCode.Shift, Code.A)); + + // Then + hotkeysContext.Keys + .Select(hotkey => string.Join("+", hotkey.ToStringKeys())) + .Is("Shift+A", "F1", "Shift+A", "F1"); + } + + [Test] + public void Remove_by_Filter_Test() + { + // Given + using var hotkeysContext = new HotKeysContext(Task.FromResult(default(IJSObjectReference)!), NullLogger.Instance) + .Add(ModCode.Shift, Code.A, () => { }, exclude: Exclude.ContentEditable) + .Add(Key.F1, () => { }) + .Add(ModCode.Shift, Code.A, () => { }, exclude: Exclude.InputNonText) + .Add(Key.F1, () => { }); + + // When + hotkeysContext.Remove(entries => + { + return entries.Where(e => e is HotKeyEntryByCode codeEntry && codeEntry.Code == Code.A); + }); + + // Then + hotkeysContext.Keys + .Select(hotkey => string.Join("+", hotkey.ToStringKeys())) + .Is("F1", "F1"); + } +} diff --git a/HotKeys2/HotKeysContext.cs b/HotKeys2/HotKeysContext.cs index aef25d1..d0b9cb0 100644 --- a/HotKeys2/HotKeysContext.cs +++ b/HotKeys2/HotKeysContext.cs @@ -644,8 +644,14 @@ private void Unregister(HotKeyEntry hotKeyEntry) }); } + private const string _AMBIGUOUS_PARAMETER_EXCEPTION_MESSAGE = "Specified parameters are ambiguous to identify the single hotkey entry that should be removed."; + /// - /// Remove one or more hotkey entries from this context. + /// Remove a hotkey entriy from this context.
+ /// If the parameter cannot find any hotkey entry, this method will return without exception.
+ /// If only one hotkey entry can be identified by the parameter, it will be removed even if the other parameters are not matched.
+ /// If the parameter identifies two or more hotkey entries, the other parameters are referenced to identify the single hotkey entry to be removed.
+ /// If the parameters can not determine a single hotkey entry, the exception will be thrown. ///
/// The identifier of hotkey. /// The description of the meaning of this hot key entry. @@ -655,8 +661,13 @@ private void Unregister(HotKeyEntry hotKeyEntry) public HotKeysContext Remove(Key key, string description = "", Exclude exclude = Exclude.Default, string excludeSelector = "") => this.Remove(ModKey.None, key, description, exclude, excludeSelector); + /// - /// Remove one or more hotkey entries from this context. + /// Remove a hotkey entriy from this context.
+ /// If the combination of the and the parameters cannot find any hotkey entry, this method will return without exception.
+ /// If only one hotkey entry can be identified by the combination of the and the parameters, it will be removed even if the other parameters are not matched.
+ /// If the combination of the and the parameters identifies two or more hotkey entries, the other parameters are referenced to identify the single hotkey entry to be removed.
+ /// If the parameters can not determine a single hotkey entry, the exception will be thrown. ///
/// The combination of modifier keys flags. /// The identifier of hotkey. @@ -667,18 +678,25 @@ public HotKeysContext Remove(Key key, string description = "", Exclude exclude = public HotKeysContext Remove(ModKey modifiers, Key key, string description = "", Exclude exclude = Exclude.Default, string excludeSelector = "") { var keyEntry = key.ToString(); - return this.Remove(keys => keys - .OfType() - .Where( - k => k.Modifiers == modifiers && - k.Key.ToString() == keyEntry && - k.Description == description && - k.Exclude == exclude && - k.ExcludeSelector == excludeSelector)); + return this.Remove(keys => + { + var removeCandidates = keys.OfType().Where(k => k.Modifiers == modifiers && k.Key.ToString() == keyEntry).ToArray(); + if (removeCandidates.Length <= 1) return removeCandidates; + removeCandidates = removeCandidates.Where(k => k.Exclude == exclude && k.ExcludeSelector == excludeSelector).ToArray(); + if (removeCandidates.Length == 1) return removeCandidates; + if (removeCandidates.Length == 0) throw new ArgumentException(_AMBIGUOUS_PARAMETER_EXCEPTION_MESSAGE); + removeCandidates = removeCandidates.Where(k => k.Description == description).ToArray(); + if (removeCandidates.Length == 1) return removeCandidates; + throw new ArgumentException(_AMBIGUOUS_PARAMETER_EXCEPTION_MESSAGE); + }); } /// - /// Remove one or more hotkey entries from this context. + /// Remove a hotkey entriy from this context.
+ /// If the parameter cannot find any hotkey entry, this method will return without exception.
+ /// If only one hotkey entry can be identified by the parameter, it will be removed even if the other parameters are not matched.
+ /// If the parameter identifies two or more hotkey entries, the other parameters are referenced to identify the single hotkey entry to be removed.
+ /// If the parameters can not determine a single hotkey entry, the exception will be thrown. ///
/// The identifier of hotkey. /// The description of the meaning of this hot key entry. @@ -689,7 +707,11 @@ public HotKeysContext Remove(Code code, string description = "", Exclude exclude => this.Remove(ModCode.None, code, description, exclude, excludeSelector); /// - /// Remove one or more hotkey entries from this context. + /// Remove a hotkey entriy from this context.
+ /// If the combination of the and the parameters cannot find any hotkey entry, this method will return without exception.
+ /// If only one hotkey entry can be identified by the combination of the and the parameters, it will be removed even if the other parameters are not matched.
+ /// If the combination of the and the parameters identifies two or more hotkey entries, the other parameters are referenced to identify the single hotkey entry to be removed.
+ /// If the parameters can not determine a single hotkey entry, the exception will be thrown. ///
/// The combination of modifier keys flags. /// The identifier of hotkey. @@ -700,17 +722,24 @@ public HotKeysContext Remove(Code code, string description = "", Exclude exclude public HotKeysContext Remove(ModCode modifiers, Code code, string description = "", Exclude exclude = Exclude.Default, string excludeSelector = "") { var keyEntry = code.ToString(); - return this.Remove(keys => keys - .OfType() - .Where( - k => k.Modifiers == modifiers && - k.Code.ToString() == keyEntry && - k.Description == description && - k.ExcludeSelector == excludeSelector && - k.Exclude == exclude)); + return this.Remove(keys => + { + var removeCandidates = keys.OfType().Where(k => k.Modifiers == modifiers && k.Code.ToString() == keyEntry).ToArray(); + if (removeCandidates.Length <= 1) return removeCandidates; + removeCandidates = removeCandidates.Where(k => k.ExcludeSelector == excludeSelector && k.Exclude == exclude).ToArray(); + if (removeCandidates.Length == 1) return removeCandidates; + if (removeCandidates.Length == 0) throw new ArgumentException(_AMBIGUOUS_PARAMETER_EXCEPTION_MESSAGE); + removeCandidates = removeCandidates.Where(k => k.Description == description).ToArray(); + if (removeCandidates.Length == 1) return removeCandidates; + throw new ArgumentException(_AMBIGUOUS_PARAMETER_EXCEPTION_MESSAGE); + }); } - private HotKeysContext Remove(Func, IEnumerable> filter) + /// + /// Remove all hotkey entries from this context where the function returns true. + /// + /// + public HotKeysContext Remove(Func, IEnumerable> filter) { var entries = filter.Invoke(this.Keys).ToArray(); foreach (var entry in entries) diff --git a/README.md b/README.md index 10eabe8..4c5dc5d 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,58 @@ You can also specify the elements that are disabled hotkeys by CSS query selecto And you can specify the `Exclude.ContentEditable` to register the unavailable hotkey when any "contenteditable" applied elements have focus. +### How to remove hotkeys + +You can remove hotkkey entries by calling the `Remove()` method of the `HotKeysContext` object, like this. + +```csharp +this.HotKeysContext.Remove(ModCode.Ctrl, Code.A); +``` + +Please remember that the `Remove` method will remove a hotkey entry identified by the `key`, `code`, and `modifiers` parameters even if other parameters are unmatched by the registered hotkey entry as long as it can identify a single hotkey entry. + +```csharp +... + this.HotKeys.CreateContext() + .Add(Code.A, OnKeyDownA, exclude: Exclude.InputNonText | Exclude.TextArea); +... +// The following code will remove the hotkey entry registered by the above code +// even though the "exclude" option is different. +this.HotKeysContext.Remove(Code.A); +``` + +If the parameters for the `Remove` method can not determine a single hotkey entry, the `ArgumentException` exception will be thrown. + + +```csharp +... + this.HotKeys.CreateContext() + .Add(Code.A, OnKeyDownAForTextArea, exclude: Exclude.InputNonText | Exclude.InputText) + .Add(Code.A, OnKeyDownAForInputText, exclude: Exclude.InputNonText | Exclude.TextArea); +... +// The following code will throw an ArgumentException exception +// because the "Remove" method can not determine a single hotkey entry. +this.HotKeysContext.Remove(Code.A); +... +// The following code will successfully remove the hotkey entry in the second one. +this.HotKeysContext.Remove(Code.A, exclude: Exclude.InputNonText | Exclude.TextArea); +``` + +If the `key`, `code`, and `modifires` parameters cannot find any hotkey entry, the `Remove` method will return without exception. + +The `HotKeysContext` also provides another `Remove` method overload version that accepts a filter function as an argument to determine which hotkey entries to remove. This method will remove all hotkey entries in which the filter function returns. + +```csharp +// The following code will remove all hotkey entries registered by the "Code. A", +// regardless of what modifiers, exclude options, etc. +this.HotKeysContext.Remove(entries => +{ + return entries.Where(e => e is HotKeyEntryByCode codeEntry && codeEntry.Code == Code.A); +}); +``` + + + ## `Code` vs. `Key` - which way should I use to? There are two ways to register hotkeys in the `HotKeysContext`.