diff --git a/HotKeys2.Test/HotKeysContextTest.cs b/HotKeys2.Test/HotKeysContextTest.cs index ddb1530..24f3d1d 100644 --- a/HotKeys2.Test/HotKeysContextTest.cs +++ b/HotKeys2.Test/HotKeysContextTest.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.JSInterop; namespace Toolbelt.Blazor.HotKeys2.Test; @@ -9,7 +8,7 @@ public class HotKeysContextTest public void Remove_by_Key_Test() { // Given - using var hotkeysContext = new HotKeysContext(Task.FromResult(default(IJSObjectReference)!), NullLogger.Instance) + using var hotkeysContext = new HotKeysContext(null!, 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, () => { }) @@ -19,7 +18,7 @@ public void Remove_by_Key_Test() hotkeysContext.Remove(Key.F1); // Then - hotkeysContext.Keys + hotkeysContext.HotKeyEntries .Select(hotkey => string.Join("+", hotkey.ToStringKeys())) .Is("A", "Shift+A", "Ctrl+Alt+F1"); } @@ -28,7 +27,7 @@ public void Remove_by_Key_Test() public void Remove_by_Code_and_Mod_Test() { // Given - using var hotkeysContext = new HotKeysContext(Task.FromResult(default(IJSObjectReference)!), NullLogger.Instance) + using var hotkeysContext = new HotKeysContext(null!, 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. @@ -38,7 +37,7 @@ public void Remove_by_Code_and_Mod_Test() hotkeysContext.Remove(ModCode.Shift, Code.A); // Then - hotkeysContext.Keys + hotkeysContext.HotKeyEntries .Select(hotkey => string.Join("+", hotkey.ToStringKeys())) .Is("A", "F1", "Ctrl+Alt+F1"); } @@ -47,7 +46,7 @@ public void Remove_by_Code_and_Mod_Test() public void Remove_by_Key_and_Exclude_Test() { // Given - using var hotkeysContext = new HotKeysContext(Task.FromResult(default(IJSObjectReference)!), NullLogger.Instance) + using var hotkeysContext = new HotKeysContext(null!, NullLogger.Instance) .Add(Code.A, () => { }) .Add(ModKey.Meta, Key.F1, () => { }, new() { Exclude = Exclude.ContentEditable }) .Add(ModCode.Shift, Code.A, () => { }) @@ -57,7 +56,7 @@ public void Remove_by_Key_and_Exclude_Test() hotkeysContext.Remove(ModKey.Meta, Key.F1, exclude: Exclude.ContentEditable); // Then - hotkeysContext.Keys + hotkeysContext.HotKeyEntries .Select(hotkey => string.Join("+", hotkey.ToStringKeys())) .Is("A", "Shift+A", "Meta+F1"); } @@ -66,7 +65,7 @@ public void Remove_by_Key_and_Exclude_Test() public void Remove_by_Code_and_ExcludeSelector_Test() { // Given - using var hotkeysContext = new HotKeysContext(Task.FromResult(default(IJSObjectReference)!), NullLogger.Instance) + using var hotkeysContext = new HotKeysContext(null!, NullLogger.Instance) .Add(Code.A, () => { }) .Add(ModKey.Meta, Key.F1, () => { }) .Add(Code.A, () => { }, new() { ExcludeSelector = "[data-no-hotkeys]" }) @@ -76,7 +75,7 @@ public void Remove_by_Code_and_ExcludeSelector_Test() hotkeysContext.Remove(Code.A, excludeSelector: "[data-no-hotkeys]"); // Then - hotkeysContext.Keys + hotkeysContext.HotKeyEntries .Select(hotkey => string.Join("+", hotkey.ToStringKeys())) .Is("A", "Meta+F1", "Meta+F1"); } @@ -85,7 +84,7 @@ public void Remove_by_Code_and_ExcludeSelector_Test() public void Remove_by_Key_but_Ambiguous_Exception_Test() { // Given - using var hotkeysContext = new HotKeysContext(Task.FromResult(default(IJSObjectReference)!), NullLogger.Instance) + using var hotkeysContext = new HotKeysContext(null!, NullLogger.Instance) .Add(ModCode.Shift, Code.A, () => { }) .Add(Key.F1, () => { }, exclude: Exclude.ContentEditable) .Add(ModCode.Shift, Code.A, () => { }) @@ -95,7 +94,7 @@ public void Remove_by_Key_but_Ambiguous_Exception_Test() Assert.Throws(() => hotkeysContext.Remove(Key.F1)); // Then - hotkeysContext.Keys + hotkeysContext.HotKeyEntries .Select(hotkey => string.Join("+", hotkey.ToStringKeys())) .Is("Shift+A", "F1", "Shift+A", "F1"); } @@ -104,7 +103,7 @@ public void Remove_by_Key_but_Ambiguous_Exception_Test() public void Remove_by_Code_but_Ambiguous_Exception_Test() { // Given - using var hotkeysContext = new HotKeysContext(Task.FromResult(default(IJSObjectReference)!), NullLogger.Instance) + using var hotkeysContext = new HotKeysContext(null!, NullLogger.Instance) .Add(ModCode.Shift, Code.A, () => { }, exclude: Exclude.ContentEditable) .Add(Key.F1, () => { }) .Add(ModCode.Shift, Code.A, () => { }, exclude: Exclude.InputNonText) @@ -114,7 +113,7 @@ public void Remove_by_Code_but_Ambiguous_Exception_Test() Assert.Throws(() => hotkeysContext.Remove(ModCode.Shift, Code.A)); // Then - hotkeysContext.Keys + hotkeysContext.HotKeyEntries .Select(hotkey => string.Join("+", hotkey.ToStringKeys())) .Is("Shift+A", "F1", "Shift+A", "F1"); } @@ -123,7 +122,7 @@ public void Remove_by_Code_but_Ambiguous_Exception_Test() public void Remove_by_Filter_Test() { // Given - using var hotkeysContext = new HotKeysContext(Task.FromResult(default(IJSObjectReference)!), NullLogger.Instance) + using var hotkeysContext = new HotKeysContext(null!, NullLogger.Instance) .Add(ModCode.Shift, Code.A, () => { }, exclude: Exclude.ContentEditable) .Add(Key.F1, () => { }) .Add(ModCode.Shift, Code.A, () => { }, exclude: Exclude.InputNonText) @@ -136,7 +135,7 @@ public void Remove_by_Filter_Test() }); // Then - hotkeysContext.Keys + hotkeysContext.HotKeyEntries .Select(hotkey => string.Join("+", hotkey.ToStringKeys())) .Is("F1", "F1"); } diff --git a/HotKeys2/Extensions/JS.cs b/HotKeys2/Extensions/JS.cs new file mode 100644 index 0000000..48a3855 --- /dev/null +++ b/HotKeys2/Extensions/JS.cs @@ -0,0 +1,29 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; +using Microsoft.JSInterop; + +namespace Toolbelt.Blazor.HotKeys2.Extensions; + +[SuppressMessage("Usage", "CA2254:Template should be a static expression")] +internal static class JS +{ + public static async ValueTask ImportScriptAsync(this IJSRuntime jsRuntime, ILogger logger) + { + var scriptPath = "./_content/Toolbelt.Blazor.HotKeys2/script.min.js"; + try + { + var isOnLine = await jsRuntime.InvokeAsync("Toolbelt.Blazor.getProperty", "navigator.onLine"); + if (isOnLine) scriptPath += "?v=" + VersionInfo.VersionText; + } + catch (JSException e) { logger.LogError(e, e.Message); } + + return await jsRuntime.InvokeAsync("import", scriptPath); + } + + public static async ValueTask InvokeSafeAsync(Func action, ILogger logger) + { + try { await action(); } + catch (JSDisconnectedException) { } // Ignore this exception because it is thrown when the user navigates to another page. + catch (Exception ex) { logger.LogError(ex, ex.Message); } + } +} diff --git a/HotKeys2/Extensions/SemaphoreSlimExtensions.cs b/HotKeys2/Extensions/SemaphoreSlimExtensions.cs new file mode 100644 index 0000000..5be2bcf --- /dev/null +++ b/HotKeys2/Extensions/SemaphoreSlimExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Logging; + +namespace Toolbelt.Blazor.HotKeys2.Extensions; + +internal static class SemaphoreSlimExtensions +{ + public static async ValueTask InvokeAsync(this SemaphoreSlim semaphore, Func> asyncAction, ILogger? logger = null) + { + await semaphore.WaitAsync(); + try { return await asyncAction.Invoke(); } + catch (Exception ex) + { + if (logger != null) + { + logger.LogError(ex, ex.Message); + return default!; + } + throw; + } + finally { semaphore.Release(); } + } +} diff --git a/HotKeys2/HotKeyEntry.cs b/HotKeys2/HotKeyEntry.cs index 2b69c61..201dfe7 100644 --- a/HotKeys2/HotKeyEntry.cs +++ b/HotKeys2/HotKeyEntry.cs @@ -60,6 +60,8 @@ public abstract class HotKeyEntry : IDisposable private readonly ILogger? _Logger; + private bool _Disposed = false; + /// /// Notifies when the property values of the state object has changed. /// @@ -161,12 +163,16 @@ public string[] ToStringKeys() } /// - /// Disposes the hot key entry. + /// [Don't use this method. This method is for internal use only.] /// + [EditorBrowsable(EditorBrowsableState.Never)] public void Dispose() { + if (this._Disposed) return; + GC.SuppressFinalize(this); this.State._NotifyStateChanged = null; this.Id = -1; this._ObjectRef.Dispose(); + this._Disposed = true; } } diff --git a/HotKeys2/HotKeys.cs b/HotKeys2/HotKeys.cs index a79b3d9..b6616ff 100644 --- a/HotKeys2/HotKeys.cs +++ b/HotKeys2/HotKeys.cs @@ -1,9 +1,8 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; using Microsoft.JSInterop; +using Toolbelt.Blazor.HotKeys2.Extensions; namespace Toolbelt.Blazor.HotKeys2; @@ -12,22 +11,34 @@ namespace Toolbelt.Blazor.HotKeys2; /// public class HotKeys : IAsyncDisposable { - private volatile bool _Attached = false; - private readonly ILogger _Logger; private readonly IJSRuntime _JSRuntime; - private IJSObjectReference? _JSModule = null; + internal readonly DotNetObjectReference _ObjectRef; private readonly SemaphoreSlim _Syncer = new(1, 1); - private readonly bool _IsWasm = RuntimeInformation.OSDescription == "web" || RuntimeInformation.OSDescription == "Browser"; + private EventHandler? _KeyDown; /// /// Occurs when the user enter any keys on the browser. /// - public event EventHandler? KeyDown; + public event EventHandler? KeyDown + { + add + { + this._KeyDown += value; + var _ = this.EnsureAttachedAsync(); + } + remove + { + this._KeyDown -= value; + if (this._KeyDown == null) { var _ = this.DetachAsync(); } + } + } + + private IJSObjectReference? _KeyEventHandler; /// /// Initialize a new instance of the HotKeys class. @@ -35,53 +46,45 @@ public class HotKeys : IAsyncDisposable [DynamicDependency(nameof(OnKeyDown), typeof(HotKeys))] internal HotKeys(IJSRuntime jSRuntime, ILogger logger) { + this._ObjectRef = DotNetObjectReference.Create(this); this._JSRuntime = jSRuntime; this._Logger = logger; } - /// - /// Attach this HotKeys service instance to JavaScript DOM event handler. - /// - private async Task AttachAsync() + private async ValueTask EnsureAttachedAsync() { - var module = await this.GetJsModuleAsync(); - if (this._Attached) return module; - await this._Syncer.WaitAsync(); - try + await this._Syncer.InvokeAsync(async () => { - if (this._Attached) return module; - - await module.InvokeAsync("Toolbelt.Blazor.HotKeys2.attach", DotNetObjectReference.Create(this), this._IsWasm); - - this._Attached = true; - return module; - } - finally { this._Syncer.Release(); } - } + if (this._KeyEventHandler != null) return true; - private string GetVersionText() - { - var assembly = this.GetType().Assembly; - return assembly - .GetCustomAttribute()? - .InformationalVersion ?? assembly.GetName().Version?.ToString() ?? "0.0.0"; + await JS.InvokeSafeAsync(async () => + { + await using var module = await this._JSRuntime.ImportScriptAsync(this._Logger); + this._KeyEventHandler = await module.InvokeAsync( + "Toolbelt.Blazor.HotKeys2.handleKeyEvent", + this._ObjectRef, + OperatingSystem.IsBrowser()); + }, this._Logger); + + return true; + }, this._Logger); } - private async ValueTask GetJsModuleAsync() + private async ValueTask DetachAsync() { - if (this._JSModule == null) + await this._Syncer.InvokeAsync(async () => { - var scriptPath = "./_content/Toolbelt.Blazor.HotKeys2/script.min.js"; - try + if (this._KeyEventHandler != null) { - var isOnLine = await this._JSRuntime.InvokeAsync("Toolbelt.Blazor.getProperty", "navigator.onLine"); - if (isOnLine) scriptPath += $"?v={this.GetVersionText()}"; + await JS.InvokeSafeAsync(async () => + { + await this._KeyEventHandler.InvokeVoidAsync("dispose"); + await this._KeyEventHandler.DisposeAsync(); + }, this._Logger); + this._KeyEventHandler = null; } - catch (JSException e) { this._Logger.LogError(e, e.Message); } - - this._JSModule = await this._JSRuntime.InvokeAsync("import", scriptPath); - } - return this._JSModule; + return true; + }, this._Logger); } /// @@ -90,8 +93,7 @@ private async ValueTask GetJsModuleAsync() /// public HotKeysContext CreateContext() { - var attachTask = this.AttachAsync(); - return new HotKeysContext(attachTask, this._Logger); + return new HotKeysContext(this._JSRuntime, this._Logger); } /// @@ -106,15 +108,15 @@ public HotKeysContext CreateContext() [JSInvokable(nameof(OnKeyDown)), EditorBrowsable(EditorBrowsableState.Never)] public bool OnKeyDown(ModCode modifiers, string srcElementTagName, string srcElementTypeName, string key, string code) { - var args = new HotKeyDownEventArgs(modifiers, srcElementTagName, srcElementTypeName, this._IsWasm, key, code); - KeyDown?.Invoke(null, args); + var args = new HotKeyDownEventArgs(modifiers, srcElementTagName, srcElementTypeName, OperatingSystem.IsBrowser(), key, code); + this._KeyDown?.Invoke(null, args); return args.PreventDefault; } public async ValueTask DisposeAsync() { - try { if (this._JSModule != null) await this._JSModule.DisposeAsync(); } - catch (Exception ex) when (ex.GetType().FullName == "Microsoft.JSInterop.JSDisconnectedException") { } - catch (Exception ex) { this._Logger.LogError(ex, ex.Message); } + GC.SuppressFinalize(this); + await this.DetachAsync(); + this._ObjectRef.Dispose(); } } diff --git a/HotKeys2/HotKeysContext.cs b/HotKeys2/HotKeysContext.cs index 0b97388..d32cdfd 100644 --- a/HotKeys2/HotKeysContext.cs +++ b/HotKeys2/HotKeysContext.cs @@ -1,32 +1,53 @@ -using Microsoft.AspNetCore.Components; +using System.ComponentModel; +using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Logging; using Microsoft.JSInterop; +using Toolbelt.Blazor.HotKeys2.Extensions; namespace Toolbelt.Blazor.HotKeys2; /// /// Current active hotkeys set. /// -public partial class HotKeysContext : IDisposable +public partial class HotKeysContext : IDisposable, IAsyncDisposable { + private readonly List _Keys = []; + /// /// The collection of Hotkey entries. /// - public List Keys { get; } = new List(); + public IEnumerable HotKeyEntries => this._Keys; + + [Obsolete("Use the HotKeyEntries instead."), EditorBrowsable(EditorBrowsableState.Never)] + public List Keys => this._Keys; - private readonly Task _AttachTask; + private readonly IJSRuntime _JSRuntime; private readonly ILogger _Logger; - private bool _IsDisposed = false; + private readonly SemaphoreSlim _Syncer = new(1, 1); + + private readonly Task _JSContextTask; + + private bool _Disposed = false; /// /// Initialize a new instance of the HotKeysContext class. /// - internal HotKeysContext(Task attachTask, ILogger logger) + internal HotKeysContext(IJSRuntime jsRuntime, ILogger logger) { - this._AttachTask = attachTask; + this._JSRuntime = jsRuntime; this._Logger = logger; + this._JSContextTask = this.CreateJSContextAsync(); + } + + private async Task CreateJSContextAsync() + { + return await this._Syncer.InvokeAsync(async () => + { + await using var module = await this._JSRuntime.ImportScriptAsync(this._Logger); + return await module.InvokeAsync("Toolbelt.Blazor.HotKeys2.createContext"); + }); } // =============================================================================================== @@ -593,7 +614,7 @@ private HotKeysContext AddInternal(ModCode modifiers, Code code, Func + await this._Syncer.InvokeAsync(async () => { - var module = await this._AttachTask; - if (this._IsDisposed) return; + if (this._Disposed) return false; - hotKeyEntry.Id = await module.InvokeAsync( - "Toolbelt.Blazor.HotKeys2.register", - hotKeyEntry._ObjectRef, hotKeyEntry.Mode, hotKeyEntry._Modifiers, hotKeyEntry._KeyEntry, hotKeyEntry.Exclude, hotKeyEntry.ExcludeSelector, hotKeyEntry.State.Disabled); + await JS.InvokeSafeAsync(async () => + { + var context = await this._JSContextTask; + hotKeyEntry.Id = await context.InvokeAsync( + "register", + hotKeyEntry._ObjectRef, hotKeyEntry.Mode, hotKeyEntry._Modifiers, hotKeyEntry._KeyEntry, hotKeyEntry.Exclude, hotKeyEntry.ExcludeSelector, hotKeyEntry.State.Disabled); + }, this._Logger); + + return true; }); } - private void OnNotifyStateChanged(HotKeyEntry hotKeyEntry) + private async void OnNotifyStateChanged(HotKeyEntry hotKeyEntry) { - var _ = this.InvokeJsSafeAsync(async () => + await this._Syncer.InvokeAsync(async () => { - var module = await this._AttachTask; - await module.InvokeVoidAsync("Toolbelt.Blazor.HotKeys2.update", hotKeyEntry.Id, hotKeyEntry.State.Disabled); + if (this._Disposed) return false; + + await JS.InvokeSafeAsync(async () => + { + var context = await this._JSContextTask; + await context.InvokeVoidAsync("update", hotKeyEntry.Id, hotKeyEntry.State.Disabled); + }, this._Logger); + return true; }); } private async ValueTask UnregisterAsync(HotKeyEntry hotKeyEntry) { - await this.InvokeJsSafeAsync(async () => + await JS.InvokeSafeAsync(async () => { - var module = await this._AttachTask; - await module.InvokeVoidAsync("Toolbelt.Blazor.HotKeys2.unregister", hotKeyEntry.Id); - }); - - await this.InvokeJsSafeAsync(() => { hotKeyEntry.Dispose(); return ValueTask.CompletedTask; }); - } - - private async ValueTask InvokeJsSafeAsync(Func action) - { - try { await action(); } - catch (JSDisconnectedException) { } // Ignore this exception because it is thrown when the user navigates to another page. - catch (Exception ex) { this._Logger.LogError(ex, ex.Message); } + var context = await this._JSContextTask; + await context.InvokeVoidAsync("unregister", hotKeyEntry.Id); + }, this._Logger); + hotKeyEntry.Dispose(); } private const string _AMBIGUOUS_PARAMETER_EXCEPTION_MESSAGE = "Specified parameters are ambiguous to identify the single hotkey entry that should be removed."; @@ -739,12 +763,18 @@ public HotKeysContext Remove(ModCode modifiers, Code code, string description = /// public HotKeysContext Remove(Func, IEnumerable> filter) { - var entries = filter.Invoke(this.Keys).ToArray(); + var entries = filter.Invoke(this._Keys).ToArray(); foreach (var entry in entries) { - var _ = this.UnregisterAsync(entry); - lock (this.Keys) this.Keys.Remove(entry); + lock (this._Keys) this._Keys.Remove(entry); entry._NotifyStateChanged = null; + + var _ = this._Syncer.InvokeAsync(async () => + { + if (this._Disposed) return false; + await this.UnregisterAsync(entry); + return true; + }, this._Logger); } return this; } @@ -752,14 +782,35 @@ public HotKeysContext Remove(Func, IEnumerable /// Deactivate the hot key entry contained in this context. /// - public void Dispose() + [Obsolete("Use the DisposeAsync instead."), EditorBrowsable(EditorBrowsableState.Never)] +#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize + public void Dispose() { var _ = this.DisposeAsync(); } +#pragma warning restore CA1816 // Dispose methods should call SuppressFinalize + + /// + /// Deactivate the hot key entry contained in this context. + /// + public async ValueTask DisposeAsync() { - this._IsDisposed = true; - foreach (var entry in this.Keys) + GC.SuppressFinalize(this); + await this._Syncer.InvokeAsync(async () => { - var _ = this.UnregisterAsync(entry); - entry._NotifyStateChanged = null; - } - this.Keys.Clear(); + if (this._Disposed) return false; + + var context = await this._JSContextTask; + this._Disposed = true; + foreach (var entry in this._Keys) + { + await this.UnregisterAsync(entry); + entry._NotifyStateChanged = null; + } + lock (this._Keys) this._Keys.Clear(); + await JS.InvokeSafeAsync(async () => + { + await context.InvokeVoidAsync("dispose"); + await context.DisposeAsync(); + }, this._Logger); + return true; + }); } } \ No newline at end of file diff --git a/HotKeys2/ILLink.Substitutions.xml b/HotKeys2/ILLink.Substitutions.xml deleted file mode 100644 index bc90618..0000000 --- a/HotKeys2/ILLink.Substitutions.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/HotKeys2/PackageSrc/Build/Toolbelt.Blazor.HotKeys2.targets b/HotKeys2/PackageSrc/Build/Toolbelt.Blazor.HotKeys2.targets deleted file mode 100644 index 007103d..0000000 --- a/HotKeys2/PackageSrc/Build/Toolbelt.Blazor.HotKeys2.targets +++ /dev/null @@ -1,11 +0,0 @@ - - - - true - - - - - - - \ No newline at end of file diff --git a/HotKeys2/PackageSrc/BuildMultiTargeting/Toolbelt.Blazor.HotKeys2.targets b/HotKeys2/PackageSrc/BuildMultiTargeting/Toolbelt.Blazor.HotKeys2.targets deleted file mode 100644 index 61c0c19..0000000 --- a/HotKeys2/PackageSrc/BuildMultiTargeting/Toolbelt.Blazor.HotKeys2.targets +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/HotKeys2/PackageSrc/BuildTransitive/Toolbelt.Blazor.HotKeys2.targets b/HotKeys2/PackageSrc/BuildTransitive/Toolbelt.Blazor.HotKeys2.targets deleted file mode 100644 index 8a731f3..0000000 --- a/HotKeys2/PackageSrc/BuildTransitive/Toolbelt.Blazor.HotKeys2.targets +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/HotKeys2/Toolbelt.Blazor.HotKeys2.csproj b/HotKeys2/Toolbelt.Blazor.HotKeys2.csproj index 4a4c6f7..b4d312a 100644 --- a/HotKeys2/Toolbelt.Blazor.HotKeys2.csproj +++ b/HotKeys2/Toolbelt.Blazor.HotKeys2.csproj @@ -6,7 +6,6 @@ enable enable nullable - $(PrepareForBuildDependsOn);UpdateILLinkSubstitutionsXml true bin\$(Configuration)\$(TargetFramework)\$(MSBuildProjectName).xml $(NoWarn);1591;NU1902 @@ -15,7 +14,7 @@ - 4.1.0.1 + 5.0.0-preview.6 Copyright © 2022-2024 J.Sakamoto, Mozilla Public License 2.0 J.Sakamoto git @@ -36,12 +35,6 @@ - - - - - ILLink.Substitutions.xml - @@ -97,6 +90,8 @@ + + ES2015 None @@ -113,10 +108,6 @@ - - - - @@ -127,5 +118,5 @@ $(PackageReleaseNotes)%0a%0aTo see all the change logs, please visit the following URL.%0a- https://github.com/jsakamoto/Toolbelt.Blazor.HotKeys2/blob/master/RELEASE-NOTES.txt - + diff --git a/HotKeys2/VersionInfo.cs b/HotKeys2/VersionInfo.cs new file mode 100644 index 0000000..52ef6f1 --- /dev/null +++ b/HotKeys2/VersionInfo.cs @@ -0,0 +1,5 @@ +namespace Toolbelt.Blazor.HotKeys2; +internal static class VersionInfo +{ + internal const string VersionText = "5.0.0-preview.6"; +} diff --git a/HotKeys2/VersionInfo.targets b/HotKeys2/VersionInfo.targets new file mode 100644 index 0000000..f85c57e --- /dev/null +++ b/HotKeys2/VersionInfo.targets @@ -0,0 +1,17 @@ + + + $(PrepareForBuildDependsOn);_GenerateVersionInfoClass + + + + + + + + + + + + + + diff --git a/HotKeys2/script.js b/HotKeys2/script.js index 912e9cf..83b9b85 100644 --- a/HotKeys2/script.js +++ b/HotKeys2/script.js @@ -4,6 +4,11 @@ export var Toolbelt; (function (Blazor) { var HotKeys2; (function (HotKeys2) { + const doc = document; + const OnKeyDownMethodName = "OnKeyDown"; + const NonTextInputTypes = ["button", "checkbox", "color", "file", "image", "radio", "range", "reset", "submit",]; + const InputTageName = "INPUT"; + const keydown = "keydown"; class HotkeyEntry { constructor(dotNetObj, mode, modifiers, keyEntry, exclude, excludeSelector, isDisabled) { this.dotNetObj = dotNetObj; @@ -18,85 +23,16 @@ export var Toolbelt; this.dotNetObj.invokeMethodAsync('InvokeAction'); } } - let idSeq = 0; - const hotKeyEntries = new Map(); - HotKeys2.register = (dotNetObj, mode, modifiers, keyEntry, exclude, excludeSelector, isDisabled) => { - const id = idSeq++; - const hotKeyEntry = new HotkeyEntry(dotNetObj, mode, modifiers, keyEntry, exclude, excludeSelector, isDisabled); - hotKeyEntries.set(id, hotKeyEntry); - return id; - }; - HotKeys2.update = (id, isDisabled) => { - const hotkeyEntry = hotKeyEntries.get(id); - if (!hotkeyEntry) - return; - hotkeyEntry.isDisabled = isDisabled; - }; - HotKeys2.unregister = (id) => { - if (id === -1) - return; - hotKeyEntries.delete(id); - }; - const convertToKeyNameMap = { - "OS": "Meta", - "Decimal": "Period", - }; + const addKeyDownEventListener = (listener) => doc.addEventListener(keydown, listener); + const removeKeyDownEventListener = (listener) => doc.removeEventListener(keydown, listener); const convertToKeyName = (ev) => { + const convertToKeyNameMap = { + "OS": "Meta", + "Decimal": "Period", + }; return convertToKeyNameMap[ev.key] || ev.key; }; - const OnKeyDownMethodName = "OnKeyDown"; - HotKeys2.attach = (hotKeysWrapper, isWasm) => { - document.addEventListener('keydown', ev => { - if (typeof (ev["altKey"]) === 'undefined') - return; - const modifiers = (ev.shiftKey ? 1 : 0) + - (ev.ctrlKey ? 2 : 0) + - (ev.altKey ? 4 : 0) + - (ev.metaKey ? 8 : 0); - const key = convertToKeyName(ev); - const code = ev.code; - const targetElement = ev.target; - const tagName = targetElement.tagName; - const type = targetElement.getAttribute('type'); - const preventDefault1 = onKeyDown(modifiers, key, code, targetElement, tagName, type); - const preventDefault2 = isWasm === true ? hotKeysWrapper.invokeMethod(OnKeyDownMethodName, modifiers, tagName, type, key, code) : false; - if (preventDefault1 || preventDefault2) - ev.preventDefault(); - if (isWasm === false) - hotKeysWrapper.invokeMethodAsync(OnKeyDownMethodName, modifiers, tagName, type, key, code); - }); - }; - const onKeyDown = (modifiers, key, code, targetElement, tagName, type) => { - let preventDefault = false; - hotKeyEntries.forEach(entry => { - if (!entry.isDisabled) { - const byCode = entry.mode === 1; - const eventKeyEntry = byCode ? code : key; - const keyEntry = entry.keyEntry; - if (keyEntry !== eventKeyEntry) - return; - const eventModkeys = byCode ? modifiers : (modifiers & (0xffff ^ 1)); - let entryModKeys = byCode ? entry.modifiers : (entry.modifiers & (0xffff ^ 1)); - if (keyEntry.startsWith("Shift") && byCode) - entryModKeys |= 1; - if (keyEntry.startsWith("Control")) - entryModKeys |= 2; - if (keyEntry.startsWith("Alt")) - entryModKeys |= 4; - if (keyEntry.startsWith("Meta")) - entryModKeys |= 8; - if (eventModkeys !== entryModKeys) - return; - if (isExcludeTarget(entry, targetElement, tagName, type)) - return; - preventDefault = true; - entry.action(); - } - }); - return preventDefault; - }; - const NonTextInputTypes = ["button", "checkbox", "color", "file", "image", "radio", "range", "reset", "submit",]; - const InputTageName = "INPUT"; + const startsWith = (str, prefix) => str.startsWith(prefix); const isExcludeTarget = (entry, targetElement, tagName, type) => { if ((entry.exclude & 1) !== 0) { if (tagName === InputTageName && NonTextInputTypes.every(t => t !== type)) @@ -118,6 +54,95 @@ export var Toolbelt; return true; return false; }; + const createKeydownHandler = (callback) => { + return (ev) => { + if (typeof (ev["altKey"]) === 'undefined') + return; + const modifiers = (ev.shiftKey ? 1 : 0) + + (ev.ctrlKey ? 2 : 0) + + (ev.altKey ? 4 : 0) + + (ev.metaKey ? 8 : 0); + const key = convertToKeyName(ev); + const code = ev.code; + const targetElement = ev.target; + const tagName = targetElement.tagName; + const type = targetElement.getAttribute('type'); + const preventDefault = callback(modifiers, key, code, targetElement, tagName, type); + if (preventDefault) + ev.preventDefault(); + }; + }; + HotKeys2.createContext = () => { + let idSeq = 0; + const hotKeyEntries = new Map(); + const onKeyDown = (modifiers, key, code, targetElement, tagName, type) => { + let preventDefault = false; + hotKeyEntries.forEach(entry => { + if (!entry.isDisabled) { + const byCode = entry.mode === 1; + const eventKeyEntry = byCode ? code : key; + const keyEntry = entry.keyEntry; + if (keyEntry !== eventKeyEntry) + return; + const eventModkeys = byCode ? modifiers : (modifiers & (0xffff ^ 1)); + let entryModKeys = byCode ? entry.modifiers : (entry.modifiers & (0xffff ^ 1)); + if (startsWith(keyEntry, "Shift") && byCode) + entryModKeys |= 1; + if (startsWith(keyEntry, "Control")) + entryModKeys |= 2; + if (startsWith(keyEntry, "Alt")) + entryModKeys |= 4; + if (startsWith(keyEntry, "Meta")) + entryModKeys |= 8; + if (eventModkeys !== entryModKeys) + return; + if (isExcludeTarget(entry, targetElement, tagName, type)) + return; + preventDefault = true; + entry.action(); + } + }); + return preventDefault; + }; + const keydownHandler = createKeydownHandler(onKeyDown); + addKeyDownEventListener(keydownHandler); + return { + register: (dotNetObj, mode, modifiers, keyEntry, exclude, excludeSelector, isDisabled) => { + const id = idSeq++; + const hotKeyEntry = new HotkeyEntry(dotNetObj, mode, modifiers, keyEntry, exclude, excludeSelector, isDisabled); + hotKeyEntries.set(id, hotKeyEntry); + return id; + }, + update: (id, isDisabled) => { + const hotkeyEntry = hotKeyEntries.get(id); + if (!hotkeyEntry) + return; + hotkeyEntry.isDisabled = isDisabled; + }, + unregister: (id) => { + if (id === -1) + return; + hotKeyEntries.delete(id); + }, + dispose: () => { removeKeyDownEventListener(keydownHandler); } + }; + }; + HotKeys2.handleKeyEvent = (hotKeysWrapper, isWasm) => { + const onKeyDown = (modifiers, key, code, targetElement, tagName, type) => { + if (isWasm) { + return hotKeysWrapper.invokeMethod(OnKeyDownMethodName, modifiers, tagName, type, key, code); + } + else { + hotKeysWrapper.invokeMethodAsync(OnKeyDownMethodName, modifiers, tagName, type, key, code); + return false; + } + }; + const keydownHandler = createKeydownHandler(onKeyDown); + addKeyDownEventListener(keydownHandler); + return { + dispose: () => { removeKeyDownEventListener(keydownHandler); } + }; + }; })(HotKeys2 = Blazor.HotKeys2 || (Blazor.HotKeys2 = {})); })(Blazor = Toolbelt.Blazor || (Toolbelt.Blazor = {})); })(Toolbelt || (Toolbelt = {})); diff --git a/HotKeys2/script.ts b/HotKeys2/script.ts index dd9b34d..8fca7d1 100644 --- a/HotKeys2/script.ts +++ b/HotKeys2/script.ts @@ -1,5 +1,7 @@ export namespace Toolbelt.Blazor.HotKeys2 { + // Constants + const enum Exclude { None = 0, InputText = 0b0001, @@ -21,6 +23,16 @@ ByCode } + const doc = document; + + const OnKeyDownMethodName = "OnKeyDown"; + + const NonTextInputTypes = ["button", "checkbox", "color", "file", "image", "radio", "range", "reset", "submit",]; + + const InputTageName = "INPUT"; + + const keydown = "keydown"; + class HotkeyEntry { constructor( @@ -38,40 +50,46 @@ } } - let idSeq: number = 0; - const hotKeyEntries = new Map(); + // Static Functions - export const register = (dotNetObj: any, mode: HotKeyMode, modifiers: ModCodes, keyEntry: string, exclude: Exclude, excludeSelector: string, isDisabled: boolean): number => { - const id = idSeq++; - const hotKeyEntry = new HotkeyEntry(dotNetObj, mode, modifiers, keyEntry, exclude, excludeSelector, isDisabled); - hotKeyEntries.set(id, hotKeyEntry); - return id; - } + const addKeyDownEventListener = (listener: (ev: KeyboardEvent) => void) => doc.addEventListener(keydown, listener); - export const update = (id: number, isDisabled: boolean): void => { - const hotkeyEntry = hotKeyEntries.get(id); - if (!hotkeyEntry) return; - hotkeyEntry.isDisabled = isDisabled; - } + const removeKeyDownEventListener = (listener: (ev: KeyboardEvent) => void) => doc.removeEventListener(keydown, listener); - export const unregister = (id: number): void => { - if (id === -1) return; - hotKeyEntries.delete(id); + const convertToKeyName = (ev: KeyboardEvent): string => { + const convertToKeyNameMap: { [key: string]: string } = { + "OS": "Meta", + "Decimal": "Period", + }; + return convertToKeyNameMap[ev.key] || ev.key; } - const convertToKeyNameMap: { [key: string]: string } = { - "OS": "Meta", - "Decimal": "Period", - }; + const startsWith = (str: string, prefix: string): boolean => str.startsWith(prefix); - const convertToKeyName = (ev: KeyboardEvent): string => { - return convertToKeyNameMap[ev.key] || ev.key; + const isExcludeTarget = (entry: HotkeyEntry, targetElement: HTMLElement, tagName: string, type: string | null): boolean => { + + if ((entry.exclude & Exclude.InputText) !== 0) { + if (tagName === InputTageName && NonTextInputTypes.every(t => t !== type)) return true; + } + if ((entry.exclude & Exclude.InputNonText) !== 0) { + if (tagName === InputTageName && NonTextInputTypes.some(t => t === type)) return true; + } + if ((entry.exclude & Exclude.TextArea) !== 0) { + if (tagName === "TEXTAREA") return true; + } + if ((entry.exclude & Exclude.ContentEditable) !== 0) { + if (targetElement.isContentEditable) return true; + } + + if (entry.excludeSelector !== '' && targetElement.matches(entry.excludeSelector)) return true; + + return false; } - const OnKeyDownMethodName = "OnKeyDown"; + type KeyEventHandler = (modifiers: ModCodes, key: string, code: string, targetElement: HTMLElement, tagName: string, type: string | null) => boolean; - export const attach = (hotKeysWrapper: any, isWasm: boolean): void => { - document.addEventListener('keydown', ev => { + const createKeydownHandler = (callback: KeyEventHandler) => { + return (ev: KeyboardEvent) => { if (typeof (ev["altKey"]) === 'undefined') return; const modifiers = (ev.shiftKey ? ModCodes.Shift : 0) + @@ -85,65 +103,89 @@ const tagName = targetElement.tagName; const type = targetElement.getAttribute('type'); - const preventDefault1 = onKeyDown(modifiers, key, code, targetElement, tagName, type); - const preventDefault2 = isWasm === true ? hotKeysWrapper.invokeMethod(OnKeyDownMethodName, modifiers, tagName, type, key, code) : false; - if (preventDefault1 || preventDefault2) ev.preventDefault(); - if (isWasm === false) hotKeysWrapper.invokeMethodAsync(OnKeyDownMethodName, modifiers, tagName, type, key, code); - }); + const preventDefault = callback(modifiers, key, code, targetElement, tagName, type); + if (preventDefault) ev.preventDefault(); + } } - const onKeyDown = (modifiers: ModCodes, key: string, code: string, targetElement: HTMLElement, tagName: string, type: string | null): boolean => { - let preventDefault = false; + export const createContext = () => { + let idSeq: number = 0; + const hotKeyEntries = new Map(); - hotKeyEntries.forEach(entry => { + const onKeyDown = (modifiers: ModCodes, key: string, code: string, targetElement: HTMLElement, tagName: string, type: string | null): boolean => { + let preventDefault = false; - if (!entry.isDisabled) { - const byCode = entry.mode === HotKeyMode.ByCode; - const eventKeyEntry = byCode ? code : key; - const keyEntry = entry.keyEntry; + hotKeyEntries.forEach(entry => { - if (keyEntry !== eventKeyEntry) return; + if (!entry.isDisabled) { + const byCode = entry.mode === HotKeyMode.ByCode; + const eventKeyEntry = byCode ? code : key; + const keyEntry = entry.keyEntry; - const eventModkeys = byCode ? modifiers : (modifiers & (0xffff ^ ModCodes.Shift)); - let entryModKeys = byCode ? entry.modifiers : (entry.modifiers & (0xffff ^ ModCodes.Shift)); - if (keyEntry.startsWith("Shift") && byCode) entryModKeys |= ModCodes.Shift; - if (keyEntry.startsWith("Control")) entryModKeys |= ModCodes.Control; - if (keyEntry.startsWith("Alt")) entryModKeys |= ModCodes.Alt; - if (keyEntry.startsWith("Meta")) entryModKeys |= ModCodes.Meta; - if (eventModkeys !== entryModKeys) return; + if (keyEntry !== eventKeyEntry) return; - if (isExcludeTarget(entry, targetElement, tagName, type)) return; + const eventModkeys = byCode ? modifiers : (modifiers & (0xffff ^ ModCodes.Shift)); + let entryModKeys = byCode ? entry.modifiers : (entry.modifiers & (0xffff ^ ModCodes.Shift)); + if (startsWith(keyEntry, "Shift") && byCode) entryModKeys |= ModCodes.Shift; + if (startsWith(keyEntry, "Control")) entryModKeys |= ModCodes.Control; + if (startsWith(keyEntry, "Alt")) entryModKeys |= ModCodes.Alt; + if (startsWith(keyEntry, "Meta")) entryModKeys |= ModCodes.Meta; + if (eventModkeys !== entryModKeys) return; - preventDefault = true; - entry.action(); - } - }); + if (isExcludeTarget(entry, targetElement, tagName, type)) return; - return preventDefault; - } + preventDefault = true; + entry.action(); + } + }); - const NonTextInputTypes = ["button", "checkbox", "color", "file", "image", "radio", "range", "reset", "submit",]; + return preventDefault; + } - const InputTageName = "INPUT"; + const keydownHandler = createKeydownHandler(onKeyDown); - const isExcludeTarget = (entry: HotkeyEntry, targetElement: HTMLElement, tagName: string, type: string | null): boolean => { + addKeyDownEventListener(keydownHandler); - if ((entry.exclude & Exclude.InputText) !== 0) { - if (tagName === InputTageName && NonTextInputTypes.every(t => t !== type)) return true; - } - if ((entry.exclude & Exclude.InputNonText) !== 0) { - if (tagName === InputTageName && NonTextInputTypes.some(t => t === type)) return true; - } - if ((entry.exclude & Exclude.TextArea) !== 0) { - if (tagName === "TEXTAREA") return true; - } - if ((entry.exclude & Exclude.ContentEditable) !== 0) { - if (targetElement.isContentEditable) return true; - } + return { + register: (dotNetObj: any, mode: HotKeyMode, modifiers: ModCodes, keyEntry: string, exclude: Exclude, excludeSelector: string, isDisabled: boolean): number => { + const id = idSeq++; + const hotKeyEntry = new HotkeyEntry(dotNetObj, mode, modifiers, keyEntry, exclude, excludeSelector, isDisabled); + hotKeyEntries.set(id, hotKeyEntry); + return id; + }, - if (entry.excludeSelector !== '' && targetElement.matches(entry.excludeSelector)) return true; + update: (id: number, isDisabled: boolean): void => { + const hotkeyEntry = hotKeyEntries.get(id); + if (!hotkeyEntry) return; + hotkeyEntry.isDisabled = isDisabled; + }, - return false; + unregister: (id: number): void => { + if (id === -1) return; + hotKeyEntries.delete(id); + }, + + dispose: (): void => { removeKeyDownEventListener(keydownHandler); } + }; } + export const handleKeyEvent = (hotKeysWrapper: any, isWasm: boolean) => { + + const onKeyDown = (modifiers: ModCodes, key: string, code: string, targetElement: HTMLElement, tagName: string, type: string | null): boolean => { + if (isWasm) { + return hotKeysWrapper.invokeMethod(OnKeyDownMethodName, modifiers, tagName, type, key, code); + } else { + hotKeysWrapper.invokeMethodAsync(OnKeyDownMethodName, modifiers, tagName, type, key, code); + return false; + } + } + + const keydownHandler = createKeydownHandler(onKeyDown); + + addKeyDownEventListener(keydownHandler); + + return { + dispose: () => { removeKeyDownEventListener(keydownHandler); } + }; + } } \ No newline at end of file diff --git a/HotKeys2/wwwroot/script.min.js b/HotKeys2/wwwroot/script.min.js index df14e1c..1d1b06f 100644 --- a/HotKeys2/wwwroot/script.min.js +++ b/HotKeys2/wwwroot/script.min.js @@ -1 +1 @@ -export var Toolbelt;(function(n){var t;(function(n){var t;(function(n){class f{constructor(n,t,i,r,u,f,e){this.dotNetObj=n;this.mode=t;this.modifiers=i;this.keyEntry=r;this.exclude=u;this.excludeSelector=f;this.isDisabled=e}action(){this.dotNetObj.invokeMethodAsync("InvokeAction")}}let e=0;const t=new Map;n.register=(n,i,r,u,o,s,h)=>{const c=e++,l=new f(n,i,r,u,o,s,h);return t.set(c,l),c};n.update=(n,i)=>{const r=t.get(n);r&&(r.isDisabled=i)};n.unregister=n=>{n!==-1&&t.delete(n)};const o={OS:"Meta",Decimal:"Period"},s=n=>o[n.key]||n.key,i="OnKeyDown";n.attach=(n,t)=>{document.addEventListener("keydown",r=>{if(typeof r.altKey!="undefined"){const u=(r.shiftKey?1:0)+(r.ctrlKey?2:0)+(r.altKey?4:0)+(r.metaKey?8:0),f=s(r),e=r.code,o=r.target,c=o.tagName,l=o.getAttribute("type"),a=h(u,f,e,o,c,l),v=t===!0?n.invokeMethod(i,u,c,l,f,e):!1;(a||v)&&r.preventDefault();t===!1&&n.invokeMethodAsync(i,u,c,l,f,e)}})};const h=(n,i,r,u,f,e)=>{let o=!1;return t.forEach(t=>{if(!t.isDisabled){const l=t.mode===1,a=l?r:i,s=t.keyEntry;if(s!==a)return;const v=l?n:n&65534;let h=l?t.modifiers:t.modifiers&65534;if(s.startsWith("Shift")&&l&&(h|=1),s.startsWith("Control")&&(h|=2),s.startsWith("Alt")&&(h|=4),s.startsWith("Meta")&&(h|=8),v!==h)return;if(c(t,u,f,e))return;o=!0;t.action()}}),o},r=["button","checkbox","color","file","image","radio","range","reset","submit",],u="INPUT",c=(n,t,i,f)=>(n.exclude&1)!=0&&i===u&&r.every(n=>n!==f)?!0:(n.exclude&2)!=0&&i===u&&r.some(n=>n===f)?!0:(n.exclude&4)!=0&&i==="TEXTAREA"?!0:(n.exclude&8)!=0&&t.isContentEditable?!0:n.excludeSelector!==""&&t.matches(n.excludeSelector)?!0:!1})(t=n.HotKeys2||(n.HotKeys2={}))})(t=n.Blazor||(n.Blazor={}))})(Toolbelt||(Toolbelt={})); \ No newline at end of file +export var Toolbelt;(function(n){var t;(function(n){var t;(function(n){const i=document,r="OnKeyDown",u=["button","checkbox","color","file","image","radio","range","reset","submit",],f="INPUT",e="keydown";class c{constructor(n,t,i,r,u,f,e){this.dotNetObj=n;this.mode=t;this.modifiers=i;this.keyEntry=r;this.exclude=u;this.excludeSelector=f;this.isDisabled=e}action(){this.dotNetObj.invokeMethodAsync("InvokeAction")}}const o=n=>i.addEventListener(e,n),s=n=>i.removeEventListener(e,n),l=n=>{return{OS:"Meta",Decimal:"Period"}[n.key]||n.key},t=(n,t)=>n.startsWith(t),a=(n,t,i,r)=>(n.exclude&1)!=0&&i===f&&u.every(n=>n!==r)?!0:(n.exclude&2)!=0&&i===f&&u.some(n=>n===r)?!0:(n.exclude&4)!=0&&i==="TEXTAREA"?!0:(n.exclude&8)!=0&&t.isContentEditable?!0:n.excludeSelector!==""&&t.matches(n.excludeSelector)?!0:!1,h=n=>t=>{if(typeof t.altKey!="undefined"){const r=(t.shiftKey?1:0)+(t.ctrlKey?2:0)+(t.altKey?4:0)+(t.metaKey?8:0),u=l(t),f=t.code,i=t.target,e=i.tagName,o=i.getAttribute("type"),s=n(r,u,f,i,e,o);s&&t.preventDefault()}};n.createContext=()=>{let r=0;const n=new Map,u=(i,r,u,f,e,o)=>{let s=!1;return n.forEach(n=>{if(!n.isDisabled){const l=n.mode===1,v=l?u:r,h=n.keyEntry;if(h!==v)return;const y=l?i:i&65534;let c=l?n.modifiers:n.modifiers&65534;if(t(h,"Shift")&&l&&(c|=1),t(h,"Control")&&(c|=2),t(h,"Alt")&&(c|=4),t(h,"Meta")&&(c|=8),y!==c)return;if(a(n,f,e,o))return;s=!0;n.action()}}),s},i=h(u);return o(i),{register:(t,i,u,f,e,o,s)=>{const h=r++,l=new c(t,i,u,f,e,o,s);return n.set(h,l),h},update:(t,i)=>{const r=n.get(t);r&&(r.isDisabled=i)},unregister:t=>{t!==-1&&n.delete(t)},dispose:()=>{s(i)}}};n.handleKeyEvent=(n,t)=>{const u=(i,u,f,e,o,s)=>t?n.invokeMethod(r,i,o,s,u,f):(n.invokeMethodAsync(r,i,o,s,u,f),!1),i=h(u);return o(i),{dispose:()=>{s(i)}}}})(t=n.HotKeys2||(n.HotKeys2={}))})(t=n.Blazor||(n.Blazor={}))})(Toolbelt||(Toolbelt={})); \ No newline at end of file diff --git a/README.md b/README.md index 10e4eff..5ed8139 100644 --- a/README.md +++ b/README.md @@ -48,15 +48,15 @@ builder.Services.AddHotKeys2(); // 👈 2. Add this line ### 2. Usage in your Blazor component (.razor) -**Step.1** Implement `IDisposable` interface to the component. +**Step.1** Implement `IAsyncDisposable` interface to the component. ```razor -@implements IDisposable @* 👈 Add this at the top of the component. *@ +@implements IAsyncDisposable @* 👈 Add this at the top of the component. *@ ... @code { ... - public void Dispose() // 👈 Add "Dispose" method. + public async ValueTask DisposeAsync() // 👈 Add "DisposeAsync" method. { } } @@ -65,7 +65,7 @@ builder.Services.AddHotKeys2(); // 👈 2. Add this line **Step.2** Open the `Toolbelt.Blazor.HotKeys2` namespace, and inject the `HotKeys` service into the component. ```razor -@implements IDisposable +@implements IAsyncDisposable @using Toolbelt.Blazor.HotKeys2 @* 👈 1. Add this *@ @inject HotKeys HotKeys @* 2. 👈 Add this *@ ... @@ -104,14 +104,17 @@ Then, you can add the combination with key and action to the `HotKeysContext` ob > The method of the callback action can take an argument which is `HotKeyEntryByCode` or `HotKeyEntryByKey` object. -**Step.4** Dispose the `HotKeysContext` object when the component is disposing, in the `Dispose()` method of the component. +**Step.4** Dispose the `HotKeysContext` object when the component is disposing, in the `DisposeAsync()` method of the component. ```csharp @code { ... - public void Dispose() + public async ValueTask DisposeAsync() { - _hotKeysContext?.Dispose(); // 👈 1. Add this + // 👇 Add this + if (_hotKeysContext != null) { + await _hotKeysContext.DisposeAsync(); + } } } ``` @@ -120,7 +123,7 @@ The complete source code (.razor) of this component is bellow. ```csharp @page "/" -@implements IDisposable +@implements IAsyncDisposable @using Toolbelt.Blazor.HotKeys2 @inject HotKeys HotKeys @@ -141,12 +144,15 @@ The complete source code (.razor) of this component is bellow. // Do something here. } - public void Dispose() + public async ValueTask DisposeAsync() { - _hotKeysContext?.Dispose(); + if (_hotKeysContext != null) { + await _hotKeysContext.DisposeAsync(); + } } } ``` + ### How to enable / disable hotkeys depending on which element has focus You can specify enabling/disabling hotkeys depending on which kind of element has focus at the hotkeys registration via a combination of the `Exclude` flags in the property of the option object argument of the `HotKeysContext.Add()` method. @@ -309,11 +315,11 @@ I recommend using the `Key` class for hotkeys registration in the following case Unlike ["angular-hotkeys"](https://github.com/chieffancypants/angular-hotkeys), this library doesn't provide "cheat sheet" feature, at this time. -Instead, the `HotKeysContext` object provides `Keys` property, so you can implement your own "Cheat Sheet" UI, like this code: +Instead, the `HotKeysContext` object provides `HotKeyEntries` property, so you can implement your own "Cheat Sheet" UI, like this code: ```razor
    - @foreach (var key in _hotKeysContext.Keys) + @foreach (var key in _hotKeysContext.HotKeyEntries) {
  • @key
  • } diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 56d49b2..4be677d 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,3 +1,9 @@ +v.5.0.0 +- Improve: Support for a round trip for interactive and non-interactive pages on .NET 8. +- Update: The IDisposable interface of the HotKeyContext class is now obsolete. Instead, use the new "DisposeAsync" method. +- Update: The "Keys" property of the HotKeyContext class is now obsolete. Instead, use the new "HotKeyEntries" property. +- Fix: unexpected exceptions due to the race condition of the JavaScript interop. + v.4.1.0.1 - Update the README to use the "OnAfterRender" method to initialize hotkeys. diff --git a/SampleSites/Components/Pages/Counter.razor b/SampleSites/Components/Pages/Counter.razor index f822222..6321e91 100644 --- a/SampleSites/Components/Pages/Counter.razor +++ b/SampleSites/Components/Pages/Counter.razor @@ -1,6 +1,6 @@ @page "/counter" @using System.Runtime.InteropServices -@implements IDisposable +@implements IAsyncDisposable @inject HotKeys HotKeys

    Counter

    @@ -77,9 +77,9 @@ } } - public void Dispose() + public async ValueTask DisposeAsync() { this.HotKeys.KeyDown -= HotKeys_KeyDown; - _hotKeysContext?.Dispose(); + if (this._hotKeysContext is not null) await this._hotKeysContext.DisposeAsync(); } } diff --git a/SampleSites/Components/Pages/Index.razor b/SampleSites/Components/Pages/Index.razor index 184c0de..8c3d5c9 100644 --- a/SampleSites/Components/Pages/Index.razor +++ b/SampleSites/Components/Pages/Index.razor @@ -1,5 +1,5 @@ @page "/" -@implements IDisposable +@implements IAsyncDisposable @inject HotKeys HotKeys

    @Greeting

    @@ -27,7 +27,7 @@ Welcome to your new app. private HotKeysContext? _hotKeysContext; - protected override void OnAfterRender(bool firstRender) + protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { @@ -37,8 +37,8 @@ Welcome to your new app. // Check the issue #20. var temporaryContext = this.HotKeys.CreateContext() - .Add(Code.U, OnHotKey); - temporaryContext.Dispose(); + .Add(Code.V, OnHotKey); + await temporaryContext.DisposeAsync(); } } @@ -55,11 +55,14 @@ Welcome to your new app. _hotKeysContext?.Remove(Code.G); } - private void OnClickDisposeHotKeyContext() + private async Task OnClickDisposeHotKeyContext() { - _hotKeysContext?.Dispose(); + if (this._hotKeysContext is not null) await this._hotKeysContext.DisposeAsync(); _hotKeysContext = null; } - public void Dispose() => _hotKeysContext?.Dispose(); + public async ValueTask DisposeAsync() + { + if (this._hotKeysContext is not null) await this._hotKeysContext.DisposeAsync(); + } } \ No newline at end of file diff --git a/SampleSites/Components/Pages/SaveText.razor.cs b/SampleSites/Components/Pages/SaveText.razor.cs index 9120f2a..46f6804 100644 --- a/SampleSites/Components/Pages/SaveText.razor.cs +++ b/SampleSites/Components/Pages/SaveText.razor.cs @@ -4,7 +4,7 @@ namespace SampleSite.Components.Pages; -public partial class SaveText : IDisposable +public partial class SaveText : IAsyncDisposable { [Inject] public HotKeys HotKeys { get; init; } = default!; @@ -16,7 +16,7 @@ public partial class SaveText : IDisposable private string _inpuText = ""; - private readonly List _savedTexts = new List(); + private readonly List _savedTexts = new(); protected override async Task OnAfterRenderAsync(bool firstRender) { @@ -35,5 +35,12 @@ private async ValueTask OnSaveText() this._savedTexts.Add(this._inpuText); } - public void Dispose() => this._hotKeysContext?.Dispose(); + public async ValueTask DisposeAsync() + { + GC.SuppressFinalize(this); + if (this._hotKeysContext is not null) + { + await this._hotKeysContext.DisposeAsync(); + } + } } diff --git a/SampleSites/Components/Pages/TestByCode.razor b/SampleSites/Components/Pages/TestByCode.razor index 591804a..7d18c80 100644 --- a/SampleSites/Components/Pages/TestByCode.razor +++ b/SampleSites/Components/Pages/TestByCode.razor @@ -1,5 +1,5 @@ @page "/test/bycode" -@implements IDisposable +@implements IAsyncDisposable @inject HotKeys HotKeys

    Test by Code

    @@ -53,8 +53,8 @@ _enteredKeyList?.OnEnteredKey(hotKeyEntry, appendLast: true); } - public void Dispose() + public async ValueTask DisposeAsync() { - _hotKeysContext?.Dispose(); + if (this._hotKeysContext is not null) await this._hotKeysContext.DisposeAsync(); } } \ No newline at end of file diff --git a/SampleSites/Components/Pages/TestByKeyName.razor b/SampleSites/Components/Pages/TestByKeyName.razor index 1ca5b4f..91767ac 100644 --- a/SampleSites/Components/Pages/TestByKeyName.razor +++ b/SampleSites/Components/Pages/TestByKeyName.razor @@ -1,5 +1,5 @@ @page "/test/bykeyname" -@implements IDisposable +@implements IAsyncDisposable @inject HotKeys HotKeys

    Test by Key Name

    @@ -30,5 +30,8 @@ _enteredKeyList?.OnEnteredKey(hotKeyEntry); } - public void Dispose() => _hotKeysContext?.Dispose(); + public async ValueTask DisposeAsync() + { + if (this._hotKeysContext is not null) await this._hotKeysContext.DisposeAsync(); + } } \ No newline at end of file diff --git a/SampleSites/Components/SampleSite.Components.csproj b/SampleSites/Components/SampleSite.Components.csproj index cae3065..1c5a5cb 100644 --- a/SampleSites/Components/SampleSite.Components.csproj +++ b/SampleSites/Components/SampleSite.Components.csproj @@ -20,7 +20,7 @@ - + diff --git a/SampleSites/Components/Shared/CheatSheet.razor b/SampleSites/Components/Shared/CheatSheet.razor index d91fb43..b37848c 100644 --- a/SampleSites/Components/Shared/CheatSheet.razor +++ b/SampleSites/Components/Shared/CheatSheet.razor @@ -1,7 +1,7 @@ 
    Hot keys cheat sheet
      - @foreach (var key in this.HotKeysContext?.Keys.Where(k => !string.IsNullOrEmpty(k.Description)) ?? Enumerable.Empty()) + @foreach (var key in this.HotKeysContext?.HotKeyEntries.Where(k => !string.IsNullOrEmpty(k.Description)) ?? Enumerable.Empty()) {
    • @key.ToString("{0}") ... @key.ToString("{1}") diff --git a/SampleSites/Components/Shared/MainLayout.razor b/SampleSites/Components/Shared/MainLayout.razor index 993892c..0969902 100644 --- a/SampleSites/Components/Shared/MainLayout.razor +++ b/SampleSites/Components/Shared/MainLayout.razor @@ -1,5 +1,5 @@ @inherits LayoutComponentBase -@implements IDisposable +@implements IAsyncDisposable @inject HotKeys HotKeys @inject NavigationManager NavigationManager @@ -77,5 +77,8 @@ _hotKeysCheatSheetVisible = visible; } - public void Dispose() => _hotKeysContext?.Dispose(); + public async ValueTask DisposeAsync() + { + if (this._hotKeysContext is not null) await this._hotKeysContext.DisposeAsync(); + } }