From 1d5bb28669a7f46a4500d04a46bdd85ba90f59e1 Mon Sep 17 00:00:00 2001 From: jsakamoto Date: Sat, 11 May 2024 11:32:31 +0900 Subject: [PATCH 1/9] Fix: unexpected exceptions due to the race condition of the JavaScript interop --- HotKeys2/Extensions.cs | 11 +++++++++ HotKeys2/HotKeyEntry.cs | 1 + HotKeys2/HotKeys.cs | 16 ++++++------ HotKeys2/HotKeysContext.cs | 50 ++++++++++++++++++++++++++------------ 4 files changed, 54 insertions(+), 24 deletions(-) create mode 100644 HotKeys2/Extensions.cs diff --git a/HotKeys2/Extensions.cs b/HotKeys2/Extensions.cs new file mode 100644 index 0000000..9a9ea1c --- /dev/null +++ b/HotKeys2/Extensions.cs @@ -0,0 +1,11 @@ +namespace Toolbelt.Blazor.HotKeys2; + +internal static class Extensions +{ + public static async ValueTask InvokeAsync(this SemaphoreSlim semaphore, Func> asyncAction) + { + await semaphore.WaitAsync(); + try { return await asyncAction.Invoke(); } + finally { semaphore.Release(); } + } +} diff --git a/HotKeys2/HotKeyEntry.cs b/HotKeys2/HotKeyEntry.cs index 2b69c61..65fc73c 100644 --- a/HotKeys2/HotKeyEntry.cs +++ b/HotKeys2/HotKeyEntry.cs @@ -165,6 +165,7 @@ public string[] ToStringKeys() /// public void Dispose() { + GC.SuppressFinalize(this); this.State._NotifyStateChanged = null; this.Id = -1; this._ObjectRef.Dispose(); diff --git a/HotKeys2/HotKeys.cs b/HotKeys2/HotKeys.cs index a79b3d9..a0d42f6 100644 --- a/HotKeys2/HotKeys.cs +++ b/HotKeys2/HotKeys.cs @@ -44,19 +44,18 @@ internal HotKeys(IJSRuntime jSRuntime, ILogger logger) /// private async Task AttachAsync() { - var module = await this.GetJsModuleAsync(); - if (this._Attached) return module; - await this._Syncer.WaitAsync(); - try + if (this._Attached && this._JSModule != null) return this._JSModule; + + return await this._Syncer.InvokeAsync(async () => { - if (this._Attached) return module; + if (this._Attached && this._JSModule != null) return this._JSModule; + var module = await this.GetJsModuleAsync(); await module.InvokeAsync("Toolbelt.Blazor.HotKeys2.attach", DotNetObjectReference.Create(this), this._IsWasm); this._Attached = true; return module; - } - finally { this._Syncer.Release(); } + }); } private string GetVersionText() @@ -113,8 +112,9 @@ public bool OnKeyDown(ModCode modifiers, string srcElementTagName, string srcEle public async ValueTask DisposeAsync() { + GC.SuppressFinalize(this); try { if (this._JSModule != null) await this._JSModule.DisposeAsync(); } - catch (Exception ex) when (ex.GetType().FullName == "Microsoft.JSInterop.JSDisconnectedException") { } + catch (JSDisconnectedException) { } catch (Exception ex) { this._Logger.LogError(ex, ex.Message); } } } diff --git a/HotKeys2/HotKeysContext.cs b/HotKeys2/HotKeysContext.cs index 0b97388..01bbc3c 100644 --- a/HotKeys2/HotKeysContext.cs +++ b/HotKeys2/HotKeysContext.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Components; +using System.ComponentModel; +using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Logging; using Microsoft.JSInterop; @@ -7,7 +8,7 @@ namespace Toolbelt.Blazor.HotKeys2; /// /// Current active hotkeys set. /// -public partial class HotKeysContext : IDisposable +public partial class HotKeysContext : IDisposable, IAsyncDisposable { /// /// The collection of Hotkey entries. @@ -18,6 +19,8 @@ public partial class HotKeysContext : IDisposable private readonly ILogger _Logger; + private readonly SemaphoreSlim _Syncer = new(1, 1); + private bool _IsDisposed = false; /// @@ -604,14 +607,18 @@ private HotKeysContext AddInternal(HotKeyEntry hotkeyEntry) private void RegisterAsync(HotKeyEntry hotKeyEntry) { - var _ = this.InvokeJsSafeAsync(async () => + var _ = this._Syncer.InvokeAsync(async () => { - var module = await this._AttachTask; - if (this._IsDisposed) return; - - 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 this.InvokeJsSafeAsync(async () => + { + if (this._IsDisposed) return; + + var module = await this._AttachTask; + hotKeyEntry.Id = await module.InvokeAsync( + "Toolbelt.Blazor.HotKeys2.register", + hotKeyEntry._ObjectRef, hotKeyEntry.Mode, hotKeyEntry._Modifiers, hotKeyEntry._KeyEntry, hotKeyEntry.Exclude, hotKeyEntry.ExcludeSelector, hotKeyEntry.State.Disabled); + }); + return true; }); } @@ -752,14 +759,25 @@ 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)] + public void Dispose() { var _ = this.DisposeAsync(); } + + /// + /// 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(); + this._IsDisposed = true; + foreach (var entry in this.Keys) + { + await this.UnregisterAsync(entry); + entry._NotifyStateChanged = null; + } + this.Keys.Clear(); + return true; + }); } } \ No newline at end of file From e490967834d921ba80f69d396c0311069dba603b Mon Sep 17 00:00:00 2001 From: jsakamoto Date: Sat, 11 May 2024 11:33:49 +0900 Subject: [PATCH 2/9] Update sample sites and README to use IAsyncDisposable, not IDisposable --- README.md | 26 ++++++++++++------- SampleSites/Components/Pages/Counter.razor | 6 ++--- SampleSites/Components/Pages/Index.razor | 17 +++++++----- .../Components/Pages/SaveText.razor.cs | 13 +++++++--- SampleSites/Components/Pages/TestByCode.razor | 6 ++--- .../Components/Pages/TestByKeyName.razor | 7 +++-- .../Components/Shared/MainLayout.razor | 7 +++-- 7 files changed, 52 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 10e4eff..7b70822 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. 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/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(); + } } From 5fe54d48062bf22198187aff397fac51a46ac042 Mon Sep 17 00:00:00 2001 From: jsakamoto Date: Sat, 11 May 2024 22:55:29 +0900 Subject: [PATCH 3/9] Improve: Support for a round trip for interactive and non-interactive pages on .NET 8. --- HotKeys2.Test/HotKeysContextTest.cs | 15 +- HotKeys2/Extensions.cs | 11 -- HotKeys2/Extensions/JS.cs | 36 ++++ .../Extensions/SemaphoreSlimExtensions.cs | 22 +++ HotKeys2/HotKeys.cs | 96 +++++----- HotKeys2/HotKeysContext.cs | 68 ++++--- HotKeys2/script.js | 177 +++++++++-------- HotKeys2/script.ts | 180 +++++++++++------- HotKeys2/wwwroot/script.min.js | 2 +- 9 files changed, 367 insertions(+), 240 deletions(-) delete mode 100644 HotKeys2/Extensions.cs create mode 100644 HotKeys2/Extensions/JS.cs create mode 100644 HotKeys2/Extensions/SemaphoreSlimExtensions.cs diff --git a/HotKeys2.Test/HotKeysContextTest.cs b/HotKeys2.Test/HotKeysContextTest.cs index ddb1530..6e565cd 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, () => { }) @@ -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. @@ -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, () => { }) @@ -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]" }) @@ -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, () => { }) @@ -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) @@ -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) diff --git a/HotKeys2/Extensions.cs b/HotKeys2/Extensions.cs deleted file mode 100644 index 9a9ea1c..0000000 --- a/HotKeys2/Extensions.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Toolbelt.Blazor.HotKeys2; - -internal static class Extensions -{ - public static async ValueTask InvokeAsync(this SemaphoreSlim semaphore, Func> asyncAction) - { - await semaphore.WaitAsync(); - try { return await asyncAction.Invoke(); } - finally { semaphore.Release(); } - } -} diff --git a/HotKeys2/Extensions/JS.cs b/HotKeys2/Extensions/JS.cs new file mode 100644 index 0000000..5bcbb72 --- /dev/null +++ b/HotKeys2/Extensions/JS.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using Microsoft.Extensions.Logging; +using Microsoft.JSInterop; + +namespace Toolbelt.Blazor.HotKeys2.Extensions; + +internal static class JS +{ + private static string GetVersionText() + { + var assembly = typeof(JS).Assembly; + return assembly + .GetCustomAttribute()? + .InformationalVersion ?? assembly.GetName().Version?.ToString() ?? "0.0.0"; + } + + 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={GetVersionText()}"; + } + 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/HotKeys.cs b/HotKeys2/HotKeys.cs index a0d42f6..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,52 +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() { - if (this._Attached && this._JSModule != null) return this._JSModule; - - return await this._Syncer.InvokeAsync(async () => + await this._Syncer.InvokeAsync(async () => { - if (this._Attached && this._JSModule != null) return this._JSModule; - - var module = await this.GetJsModuleAsync(); - await module.InvokeAsync("Toolbelt.Blazor.HotKeys2.attach", DotNetObjectReference.Create(this), this._IsWasm); - - this._Attached = true; - return module; - }); - } + 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); } /// @@ -89,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); } /// @@ -105,16 +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() { GC.SuppressFinalize(this); - try { if (this._JSModule != null) await this._JSModule.DisposeAsync(); } - catch (JSDisconnectedException) { } - catch (Exception ex) { this._Logger.LogError(ex, ex.Message); } + await this.DetachAsync(); + this._ObjectRef.Dispose(); } } diff --git a/HotKeys2/HotKeysContext.cs b/HotKeys2/HotKeysContext.cs index 01bbc3c..f4e3f48 100644 --- a/HotKeys2/HotKeysContext.cs +++ b/HotKeys2/HotKeysContext.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Logging; using Microsoft.JSInterop; +using Toolbelt.Blazor.HotKeys2.Extensions; namespace Toolbelt.Blazor.HotKeys2; @@ -15,21 +16,33 @@ public partial class HotKeysContext : IDisposable, IAsyncDisposable /// public List Keys { get; } = new List(); - private readonly Task _AttachTask; + private readonly IJSRuntime _JSRuntime; private readonly ILogger _Logger; private readonly SemaphoreSlim _Syncer = new(1, 1); + private Task _JSContextTask; + private bool _IsDisposed = 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"); + }); } // =============================================================================================== @@ -609,44 +622,37 @@ private void RegisterAsync(HotKeyEntry hotKeyEntry) { var _ = this._Syncer.InvokeAsync(async () => { - await this.InvokeJsSafeAsync(async () => - { - if (this._IsDisposed) return; + if (this._IsDisposed) return false; - var module = await this._AttachTask; - hotKeyEntry.Id = await module.InvokeAsync( - "Toolbelt.Blazor.HotKeys2.register", + 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 JS.InvokeSafeAsync(async () => { - var module = await this._AttachTask; - await module.InvokeVoidAsync("Toolbelt.Blazor.HotKeys2.update", hotKeyEntry.Id, hotKeyEntry.State.Disabled); - }); + var context = await this._JSContextTask; + await context.InvokeVoidAsync("update", hotKeyEntry.Id, hotKeyEntry.State.Disabled); + }, this._Logger); } 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."; @@ -770,6 +776,7 @@ public async ValueTask DisposeAsync() GC.SuppressFinalize(this); await this._Syncer.InvokeAsync(async () => { + var context = await this._JSContextTask; this._IsDisposed = true; foreach (var entry in this.Keys) { @@ -777,6 +784,11 @@ await this._Syncer.InvokeAsync(async () => entry._NotifyStateChanged = null; } this.Keys.Clear(); + await JS.InvokeSafeAsync(async () => + { + await context.InvokeVoidAsync("dispose"); + await context.DisposeAsync(); + }, this._Logger); return true; }); } 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 From f53dc6195c7d73ed36e29b18cca8db6a584d553b Mon Sep 17 00:00:00 2001 From: jsakamoto Date: Sat, 11 May 2024 22:57:21 +0900 Subject: [PATCH 4/9] v.5.0.0-preview.3 release --- HotKeys2/ILLink.Substitutions.xml | 2 +- HotKeys2/Toolbelt.Blazor.HotKeys2.csproj | 2 +- RELEASE-NOTES.txt | 5 +++++ SampleSites/Components/SampleSite.Components.csproj | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/HotKeys2/ILLink.Substitutions.xml b/HotKeys2/ILLink.Substitutions.xml index bc90618..d4aa2e3 100644 --- a/HotKeys2/ILLink.Substitutions.xml +++ b/HotKeys2/ILLink.Substitutions.xml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/HotKeys2/Toolbelt.Blazor.HotKeys2.csproj b/HotKeys2/Toolbelt.Blazor.HotKeys2.csproj index 4a4c6f7..a59a706 100644 --- a/HotKeys2/Toolbelt.Blazor.HotKeys2.csproj +++ b/HotKeys2/Toolbelt.Blazor.HotKeys2.csproj @@ -15,7 +15,7 @@ - 4.1.0.1 + 5.0.0-preview.3 Copyright © 2022-2024 J.Sakamoto, Mozilla Public License 2.0 J.Sakamoto git diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 56d49b2..31e694a 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,3 +1,8 @@ +v.5.0.0 +- Update: The IDisposable interface of the HotKeyContext class is now obsolete. Instead, use the new "DisposeAsync" method. +- Improve: Support for a round trip for interactive and non-interactive pages on .NET 8. +- 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/SampleSite.Components.csproj b/SampleSites/Components/SampleSite.Components.csproj index cae3065..06c92fd 100644 --- a/SampleSites/Components/SampleSite.Components.csproj +++ b/SampleSites/Components/SampleSite.Components.csproj @@ -20,7 +20,7 @@ - + From b63093a550a732edcb9d81207846aef827539a85 Mon Sep 17 00:00:00 2001 From: jsakamoto Date: Sat, 11 May 2024 23:35:20 +0900 Subject: [PATCH 5/9] Update: The "Keys" property of the HotKeyContext class is now obsolete - Instead, use the new "HotKeyEntries" property. --- HotKeys2.Test/HotKeysContextTest.cs | 14 +++++++------- HotKeys2/HotKeyEntry.cs | 3 ++- HotKeys2/HotKeysContext.cs | 7 ++++++- README.md | 4 ++-- RELEASE-NOTES.txt | 3 ++- SampleSites/Components/Shared/CheatSheet.razor | 2 +- 6 files changed, 20 insertions(+), 13 deletions(-) diff --git a/HotKeys2.Test/HotKeysContextTest.cs b/HotKeys2.Test/HotKeysContextTest.cs index 6e565cd..24f3d1d 100644 --- a/HotKeys2.Test/HotKeysContextTest.cs +++ b/HotKeys2.Test/HotKeysContextTest.cs @@ -18,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"); } @@ -37,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"); } @@ -56,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"); } @@ -75,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"); } @@ -94,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"); } @@ -113,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"); } @@ -135,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/HotKeyEntry.cs b/HotKeys2/HotKeyEntry.cs index 65fc73c..985e05c 100644 --- a/HotKeys2/HotKeyEntry.cs +++ b/HotKeys2/HotKeyEntry.cs @@ -161,8 +161,9 @@ 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() { GC.SuppressFinalize(this); diff --git a/HotKeys2/HotKeysContext.cs b/HotKeys2/HotKeysContext.cs index f4e3f48..1fdd262 100644 --- a/HotKeys2/HotKeysContext.cs +++ b/HotKeys2/HotKeysContext.cs @@ -11,10 +11,15 @@ namespace Toolbelt.Blazor.HotKeys2; /// public partial class HotKeysContext : IDisposable, IAsyncDisposable { + [Obsolete("Use the HotKeyEntries instead."), EditorBrowsable(EditorBrowsableState.Never)] + public List Keys { get; } = new List(); + /// /// The collection of Hotkey entries. /// - public List Keys { get; } = new List(); +#pragma warning disable CS0618 // Type or member is obsolete + public IEnumerable HotKeyEntries => this.Keys; +#pragma warning restore CS0618 // Type or member is obsolete private readonly IJSRuntime _JSRuntime; diff --git a/README.md b/README.md index 7b70822..5ed8139 100644 --- a/README.md +++ b/README.md @@ -315,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 31e694a..4be677d 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,6 +1,7 @@ v.5.0.0 -- Update: The IDisposable interface of the HotKeyContext class is now obsolete. Instead, use the new "DisposeAsync" method. - 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 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}") From 39515ee23ac2c7fce0a7f9ebad71a9818199b862 Mon Sep 17 00:00:00 2001 From: jsakamoto Date: Sun, 12 May 2024 21:22:04 +0900 Subject: [PATCH 6/9] Further refining the HttpContext source code --- HotKeys2/HotKeysContext.cs | 50 ++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/HotKeys2/HotKeysContext.cs b/HotKeys2/HotKeysContext.cs index 1fdd262..cd393b9 100644 --- a/HotKeys2/HotKeysContext.cs +++ b/HotKeys2/HotKeysContext.cs @@ -11,15 +11,15 @@ namespace Toolbelt.Blazor.HotKeys2; /// public partial class HotKeysContext : IDisposable, IAsyncDisposable { - [Obsolete("Use the HotKeyEntries instead."), EditorBrowsable(EditorBrowsableState.Never)] - public List Keys { get; } = new List(); + private readonly List _Keys = []; /// /// The collection of Hotkey entries. /// -#pragma warning disable CS0618 // Type or member is obsolete - public IEnumerable HotKeyEntries => this.Keys; -#pragma warning restore CS0618 // Type or member is obsolete + public IEnumerable HotKeyEntries => this._Keys; + + [Obsolete("Use the HotKeyEntries instead."), EditorBrowsable(EditorBrowsableState.Never)] + public List Keys => this._Keys; private readonly IJSRuntime _JSRuntime; @@ -27,7 +27,7 @@ public partial class HotKeysContext : IDisposable, IAsyncDisposable private readonly SemaphoreSlim _Syncer = new(1, 1); - private Task _JSContextTask; + private readonly Task _JSContextTask; private bool _IsDisposed = false; @@ -614,7 +614,7 @@ private HotKeysContext AddInternal(ModCode modifiers, Code code, Func + await this._Syncer.InvokeAsync(async () => { if (this._IsDisposed) return false; @@ -643,11 +643,17 @@ await JS.InvokeSafeAsync(async () => private async void OnNotifyStateChanged(HotKeyEntry hotKeyEntry) { - await JS.InvokeSafeAsync(async () => + await this._Syncer.InvokeAsync(async () => { - var context = await this._JSContextTask; - await context.InvokeVoidAsync("update", hotKeyEntry.Id, hotKeyEntry.State.Disabled); - }, this._Logger); + if (this._IsDisposed) 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) @@ -757,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._IsDisposed) return false; + await this.UnregisterAsync(entry); + return true; + }, this._Logger); } return this; } @@ -771,7 +783,9 @@ public HotKeysContext Remove(Func, IEnumerable [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. @@ -783,12 +797,12 @@ await this._Syncer.InvokeAsync(async () => { var context = await this._JSContextTask; this._IsDisposed = true; - foreach (var entry in this.Keys) + foreach (var entry in this._Keys) { await this.UnregisterAsync(entry); entry._NotifyStateChanged = null; } - this.Keys.Clear(); + lock (this._Keys) this._Keys.Clear(); await JS.InvokeSafeAsync(async () => { await context.InvokeVoidAsync("dispose"); From 13159e4fce619bbf7d0910a27057e99987b54851 Mon Sep 17 00:00:00 2001 From: jsakamoto Date: Sun, 12 May 2024 21:25:28 +0900 Subject: [PATCH 7/9] Generates version info text at the build time --- HotKeys2/Extensions/JS.cs | 13 +++---------- HotKeys2/ILLink.Substitutions.xml | 8 -------- .../Build/Toolbelt.Blazor.HotKeys2.targets | 11 ----------- .../Toolbelt.Blazor.HotKeys2.targets | 3 --- .../Toolbelt.Blazor.HotKeys2.targets | 3 --- HotKeys2/Toolbelt.Blazor.HotKeys2.csproj | 17 ++++------------- HotKeys2/VersionInfo.cs | 5 +++++ HotKeys2/VersionInfo.targets | 17 +++++++++++++++++ .../Components/SampleSite.Components.csproj | 2 +- 9 files changed, 30 insertions(+), 49 deletions(-) delete mode 100644 HotKeys2/ILLink.Substitutions.xml delete mode 100644 HotKeys2/PackageSrc/Build/Toolbelt.Blazor.HotKeys2.targets delete mode 100644 HotKeys2/PackageSrc/BuildMultiTargeting/Toolbelt.Blazor.HotKeys2.targets delete mode 100644 HotKeys2/PackageSrc/BuildTransitive/Toolbelt.Blazor.HotKeys2.targets create mode 100644 HotKeys2/VersionInfo.cs create mode 100644 HotKeys2/VersionInfo.targets diff --git a/HotKeys2/Extensions/JS.cs b/HotKeys2/Extensions/JS.cs index 5bcbb72..48a3855 100644 --- a/HotKeys2/Extensions/JS.cs +++ b/HotKeys2/Extensions/JS.cs @@ -1,26 +1,19 @@ -using System.Reflection; +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 { - private static string GetVersionText() - { - var assembly = typeof(JS).Assembly; - return assembly - .GetCustomAttribute()? - .InformationalVersion ?? assembly.GetName().Version?.ToString() ?? "0.0.0"; - } - 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={GetVersionText()}"; + if (isOnLine) scriptPath += "?v=" + VersionInfo.VersionText; } catch (JSException e) { logger.LogError(e, e.Message); } diff --git a/HotKeys2/ILLink.Substitutions.xml b/HotKeys2/ILLink.Substitutions.xml deleted file mode 100644 index d4aa2e3..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 a59a706..d4896ac 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 @@ - 5.0.0-preview.3 + 5.0.0-preview.5 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..887758b --- /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.5"; +} 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/SampleSites/Components/SampleSite.Components.csproj b/SampleSites/Components/SampleSite.Components.csproj index 06c92fd..3f4e67f 100644 --- a/SampleSites/Components/SampleSite.Components.csproj +++ b/SampleSites/Components/SampleSite.Components.csproj @@ -20,7 +20,7 @@ - + From 6f6e5ea74e23a8cb894c451df9139515ad4ca5d8 Mon Sep 17 00:00:00 2001 From: jsakamoto Date: Wed, 15 May 2024 10:17:04 +0900 Subject: [PATCH 8/9] Make the Dispose method safe for multiple invoking --- HotKeys2/HotKeyEntry.cs | 4 ++++ HotKeys2/HotKeysContext.cs | 12 +++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/HotKeys2/HotKeyEntry.cs b/HotKeys2/HotKeyEntry.cs index 985e05c..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. /// @@ -166,9 +168,11 @@ public string[] ToStringKeys() [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/HotKeysContext.cs b/HotKeys2/HotKeysContext.cs index cd393b9..d32cdfd 100644 --- a/HotKeys2/HotKeysContext.cs +++ b/HotKeys2/HotKeysContext.cs @@ -29,7 +29,7 @@ public partial class HotKeysContext : IDisposable, IAsyncDisposable private readonly Task _JSContextTask; - private bool _IsDisposed = false; + private bool _Disposed = false; /// /// Initialize a new instance of the HotKeysContext class. @@ -627,7 +627,7 @@ private async void RegisterAsync(HotKeyEntry hotKeyEntry) { await this._Syncer.InvokeAsync(async () => { - if (this._IsDisposed) return false; + if (this._Disposed) return false; await JS.InvokeSafeAsync(async () => { @@ -645,7 +645,7 @@ private async void OnNotifyStateChanged(HotKeyEntry hotKeyEntry) { await this._Syncer.InvokeAsync(async () => { - if (this._IsDisposed) return false; + if (this._Disposed) return false; await JS.InvokeSafeAsync(async () => { @@ -771,7 +771,7 @@ public HotKeysContext Remove(Func, IEnumerable { - if (this._IsDisposed) return false; + if (this._Disposed) return false; await this.UnregisterAsync(entry); return true; }, this._Logger); @@ -795,8 +795,10 @@ public async ValueTask DisposeAsync() GC.SuppressFinalize(this); await this._Syncer.InvokeAsync(async () => { + if (this._Disposed) return false; + var context = await this._JSContextTask; - this._IsDisposed = true; + this._Disposed = true; foreach (var entry in this._Keys) { await this.UnregisterAsync(entry); From 37e8e7e5bdff8d15c855ea1b40c02b64b2821119 Mon Sep 17 00:00:00 2001 From: jsakamoto Date: Wed, 15 May 2024 10:17:34 +0900 Subject: [PATCH 9/9] v.5.0.0-preview.6 release --- HotKeys2/Toolbelt.Blazor.HotKeys2.csproj | 2 +- HotKeys2/VersionInfo.cs | 2 +- SampleSites/Components/SampleSite.Components.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/HotKeys2/Toolbelt.Blazor.HotKeys2.csproj b/HotKeys2/Toolbelt.Blazor.HotKeys2.csproj index d4896ac..b4d312a 100644 --- a/HotKeys2/Toolbelt.Blazor.HotKeys2.csproj +++ b/HotKeys2/Toolbelt.Blazor.HotKeys2.csproj @@ -14,7 +14,7 @@ - 5.0.0-preview.5 + 5.0.0-preview.6 Copyright © 2022-2024 J.Sakamoto, Mozilla Public License 2.0 J.Sakamoto git diff --git a/HotKeys2/VersionInfo.cs b/HotKeys2/VersionInfo.cs index 887758b..52ef6f1 100644 --- a/HotKeys2/VersionInfo.cs +++ b/HotKeys2/VersionInfo.cs @@ -1,5 +1,5 @@ namespace Toolbelt.Blazor.HotKeys2; internal static class VersionInfo { - internal const string VersionText = "5.0.0-preview.5"; + internal const string VersionText = "5.0.0-preview.6"; } diff --git a/SampleSites/Components/SampleSite.Components.csproj b/SampleSites/Components/SampleSite.Components.csproj index 3f4e67f..1c5a5cb 100644 --- a/SampleSites/Components/SampleSite.Components.csproj +++ b/SampleSites/Components/SampleSite.Components.csproj @@ -20,7 +20,7 @@ - +