From 906195495080701650ee99309ea174253d5a80f5 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Tue, 25 Jan 2022 18:58:14 +0100 Subject: [PATCH 1/8] Added MarkerService --- src/LinkDotNet.Blog.Web/Pages/_Layout.cshtml | 1 + src/LinkDotNet.Blog.Web/ServiceExtensions.cs | 1 + .../Shared/Admin/CreateNewBlogPost.razor | 30 ++++++++++++++-- .../Shared/Services/IMarkerService.cs | 8 +++++ .../Shared/Services/MarkerService.cs | 35 +++++++++++++++++++ .../wwwroot/components/selection.js | 7 ++++ .../Shared/Admin/CreateNewBlogPostTests.cs | 2 +- 7 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 src/LinkDotNet.Blog.Web/Shared/Services/IMarkerService.cs create mode 100644 src/LinkDotNet.Blog.Web/Shared/Services/MarkerService.cs create mode 100644 src/LinkDotNet.Blog.Web/wwwroot/components/selection.js diff --git a/src/LinkDotNet.Blog.Web/Pages/_Layout.cshtml b/src/LinkDotNet.Blog.Web/Pages/_Layout.cshtml index 59a033ab..a6eb706e 100644 --- a/src/LinkDotNet.Blog.Web/Pages/_Layout.cshtml +++ b/src/LinkDotNet.Blog.Web/Pages/_Layout.cshtml @@ -48,6 +48,7 @@ + \ No newline at end of file diff --git a/src/LinkDotNet.Blog.Web/ServiceExtensions.cs b/src/LinkDotNet.Blog.Web/ServiceExtensions.cs index ea3c80bc..49261f24 100644 --- a/src/LinkDotNet.Blog.Web/ServiceExtensions.cs +++ b/src/LinkDotNet.Blog.Web/ServiceExtensions.cs @@ -16,5 +16,6 @@ public static void RegisterServices(this IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } } \ No newline at end of file diff --git a/src/LinkDotNet.Blog.Web/Shared/Admin/CreateNewBlogPost.razor b/src/LinkDotNet.Blog.Web/Shared/Admin/CreateNewBlogPost.razor index 6d21b6ec..ac1d197e 100644 --- a/src/LinkDotNet.Blog.Web/Shared/Admin/CreateNewBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Shared/Admin/CreateNewBlogPost.razor @@ -1,5 +1,7 @@ @using LinkDotNet.Blog.Domain +@using LinkDotNet.Blog.Web.Shared.Services @inherits MarkdownComponentBase +@inject IMarkerService markerService

@Title

@@ -15,13 +17,14 @@
- You can use markdown to style your component.
- + You can use markdown to style your component. Additional features are listed here Drag and drop markdown files to upload and @@ -83,6 +86,8 @@ private FeatureInfoDialog FeatureDialog { get; set; } + private ElementReference reference; + private CreateNewModel model = new(); protected override void OnParametersSet() @@ -114,4 +119,25 @@ { model.Content = content; } + + private async Task MarkShortDescription(KeyboardEventArgs keyboardEventArgs) + { + model.ShortDescription = await GetNewMarkdownForElementAsync(keyboardEventArgs, model.ShortDescription, "short"); + } + + private async Task MarkContent(KeyboardEventArgs keyboardEventArgs) + { + model.Content = await GetNewMarkdownForElementAsync(keyboardEventArgs, model.Content, "content"); + } + + private async Task GetNewMarkdownForElementAsync(KeyboardEventArgs keyboardEventArgs, string original, string elementId) + { + return keyboardEventArgs.CtrlKey ? keyboardEventArgs.Key switch + { + "b" => await markerService.GetNewMarkdownForElementAsync(elementId, original, "**", "**"), + "i" => await markerService.GetNewMarkdownForElementAsync(elementId, original, "*", "*"), + _ => original, + } : original; + } + } \ No newline at end of file diff --git a/src/LinkDotNet.Blog.Web/Shared/Services/IMarkerService.cs b/src/LinkDotNet.Blog.Web/Shared/Services/IMarkerService.cs new file mode 100644 index 00000000..574ad9bf --- /dev/null +++ b/src/LinkDotNet.Blog.Web/Shared/Services/IMarkerService.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace LinkDotNet.Blog.Web.Shared.Services; + +public interface IMarkerService +{ + Task GetNewMarkdownForElementAsync(string elementId, string content, string fenceBegin, string fenceEnd); +} \ No newline at end of file diff --git a/src/LinkDotNet.Blog.Web/Shared/Services/MarkerService.cs b/src/LinkDotNet.Blog.Web/Shared/Services/MarkerService.cs new file mode 100644 index 00000000..6495a358 --- /dev/null +++ b/src/LinkDotNet.Blog.Web/Shared/Services/MarkerService.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using Microsoft.JSInterop; + +namespace LinkDotNet.Blog.Web.Shared.Services; + +public class MarkerService : IMarkerService +{ + private readonly IJSRuntime jsRuntime; + + public MarkerService(IJSRuntime jsRuntime) + { + this.jsRuntime = jsRuntime; + } + + public async Task GetNewMarkdownForElementAsync(string elementId, string content, string fenceBegin, string fenceEnd) + { + var selectionRange = await jsRuntime.InvokeAsync("getSelectionFromElement", elementId); + if (selectionRange.Start == selectionRange.End) + { + return string.Empty; + } + + var beforeMarker = selectionRange.Start > 0 ? content[..selectionRange.Start] : string.Empty; + var marker = content.Substring(selectionRange.Start, selectionRange.End - selectionRange.Start); + var afterMarker = content.Substring(selectionRange.End, content.Length - selectionRange.End - 1); + return beforeMarker + fenceBegin + marker + fenceEnd + afterMarker; + } + + private sealed class SelectionRange + { + public int Start { get; set; } + + public int End { get; set; } + } +} \ No newline at end of file diff --git a/src/LinkDotNet.Blog.Web/wwwroot/components/selection.js b/src/LinkDotNet.Blog.Web/wwwroot/components/selection.js new file mode 100644 index 00000000..0f0670c8 --- /dev/null +++ b/src/LinkDotNet.Blog.Web/wwwroot/components/selection.js @@ -0,0 +1,7 @@ +window.getSelectionFromElement = function (id) { + const elem = document.getElementById(id) + const start = elem.selectionStart + const end = elem.selectionEnd + elem.selectionStart = elem.selectionEnd = 0 + return { start, end } +} \ No newline at end of file diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Admin/CreateNewBlogPostTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Admin/CreateNewBlogPostTests.cs index 1e6c44b9..122c92c4 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Admin/CreateNewBlogPostTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Admin/CreateNewBlogPostTests.cs @@ -14,7 +14,7 @@ public class CreateNewBlogPostTests : TestContext { public CreateNewBlogPostTests() { - ComponentFactories.AddStub(); + ComponentFactories.Add(): } [Fact] From 99bebe330938cd00d92d13b29757600095a59442 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Tue, 25 Jan 2022 19:35:15 +0100 Subject: [PATCH 2/8] Added tests for marker --- .../Shared/Services/MarkerService.cs | 15 +++------ .../Shared/Services/SelectionRange.cs | 8 +++++ .../wwwroot/components/selection.js | 1 - .../Pages/Admin/CreateNewBlogPostPageTests.cs | 1 + .../Pages/Admin/UpdateBlogPostPageTests.cs | 2 ++ .../Shared/Admin/CreateNewBlogPostTests.cs | 12 ++++++- .../Web/Shared/Services/MarkerServiceTests.cs | 33 +++++++++++++++++++ 7 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 src/LinkDotNet.Blog.Web/Shared/Services/SelectionRange.cs create mode 100644 tests/LinkDotNet.Blog.UnitTests/Web/Shared/Services/MarkerServiceTests.cs diff --git a/src/LinkDotNet.Blog.Web/Shared/Services/MarkerService.cs b/src/LinkDotNet.Blog.Web/Shared/Services/MarkerService.cs index 6495a358..0aa8d782 100644 --- a/src/LinkDotNet.Blog.Web/Shared/Services/MarkerService.cs +++ b/src/LinkDotNet.Blog.Web/Shared/Services/MarkerService.cs @@ -3,7 +3,7 @@ namespace LinkDotNet.Blog.Web.Shared.Services; -public class MarkerService : IMarkerService +public partial class MarkerService : IMarkerService { private readonly IJSRuntime jsRuntime; @@ -17,19 +17,12 @@ public async Task GetNewMarkdownForElementAsync(string elementId, string var selectionRange = await jsRuntime.InvokeAsync("getSelectionFromElement", elementId); if (selectionRange.Start == selectionRange.End) { - return string.Empty; + return content; } var beforeMarker = selectionRange.Start > 0 ? content[..selectionRange.Start] : string.Empty; - var marker = content.Substring(selectionRange.Start, selectionRange.End - selectionRange.Start); - var afterMarker = content.Substring(selectionRange.End, content.Length - selectionRange.End - 1); + var marker = content[selectionRange.Start..selectionRange.End]; + var afterMarker = content[selectionRange.End..]; return beforeMarker + fenceBegin + marker + fenceEnd + afterMarker; } - - private sealed class SelectionRange - { - public int Start { get; set; } - - public int End { get; set; } - } } \ No newline at end of file diff --git a/src/LinkDotNet.Blog.Web/Shared/Services/SelectionRange.cs b/src/LinkDotNet.Blog.Web/Shared/Services/SelectionRange.cs new file mode 100644 index 00000000..65e488d1 --- /dev/null +++ b/src/LinkDotNet.Blog.Web/Shared/Services/SelectionRange.cs @@ -0,0 +1,8 @@ +namespace LinkDotNet.Blog.Web.Shared.Services; + +public sealed class SelectionRange +{ + public int Start { get; set; } + + public int End { get; set; } +} \ No newline at end of file diff --git a/src/LinkDotNet.Blog.Web/wwwroot/components/selection.js b/src/LinkDotNet.Blog.Web/wwwroot/components/selection.js index 0f0670c8..05571309 100644 --- a/src/LinkDotNet.Blog.Web/wwwroot/components/selection.js +++ b/src/LinkDotNet.Blog.Web/wwwroot/components/selection.js @@ -2,6 +2,5 @@ window.getSelectionFromElement = function (id) { const elem = document.getElementById(id) const start = elem.selectionStart const end = elem.selectionEnd - elem.selectionStart = elem.selectionEnd = 0 return { start, end } } \ No newline at end of file diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Pages/Admin/CreateNewBlogPostPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Pages/Admin/CreateNewBlogPostPageTests.cs index b1ed36b4..669b8ea3 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Pages/Admin/CreateNewBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Pages/Admin/CreateNewBlogPostPageTests.cs @@ -28,6 +28,7 @@ public async Task ShouldSaveBlogPostOnSave() ctx.Services.AddScoped(_ => toastService.Object); ctx.ComponentFactories.AddStub(); ctx.Services.AddScoped(_ => Mock.Of()); + ctx.Services.AddScoped(_ => Mock.Of()); using var cut = ctx.RenderComponent(); var newBlogPost = cut.FindComponent(); diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Pages/Admin/UpdateBlogPostPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Pages/Admin/UpdateBlogPostPageTests.cs index 4c70af46..7179b2a3 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Pages/Admin/UpdateBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Pages/Admin/UpdateBlogPostPageTests.cs @@ -10,6 +10,7 @@ using LinkDotNet.Blog.Web.Pages.Admin; using LinkDotNet.Blog.Web.Shared; using LinkDotNet.Blog.Web.Shared.Admin; +using LinkDotNet.Blog.Web.Shared.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Moq; @@ -30,6 +31,7 @@ public async Task ShouldSaveBlogPostOnSave() ctx.Services.AddScoped>(_ => Repository); ctx.Services.AddScoped(_ => toastService.Object); ctx.ComponentFactories.AddStub(); + ctx.Services.AddScoped(_ => Mock.Of()); using var cut = ctx.RenderComponent( p => p.Add(s => s.BlogPostId, blogPost.Id)); var newBlogPost = cut.FindComponent(); diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Admin/CreateNewBlogPostTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Admin/CreateNewBlogPostTests.cs index 122c92c4..246fe80f 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Admin/CreateNewBlogPostTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Admin/CreateNewBlogPostTests.cs @@ -6,6 +6,9 @@ using LinkDotNet.Blog.TestUtilities; using LinkDotNet.Blog.Web.Shared; using LinkDotNet.Blog.Web.Shared.Admin; +using LinkDotNet.Blog.Web.Shared.Services; +using Microsoft.Extensions.DependencyInjection; +using Moq; using Xunit; namespace LinkDotNet.Blog.UnitTests.Web.Shared.Admin; @@ -14,12 +17,13 @@ public class CreateNewBlogPostTests : TestContext { public CreateNewBlogPostTests() { - ComponentFactories.Add(): + ComponentFactories.AddStub(); } [Fact] public void ShouldCreateNewBlogPostWhenValidDataGiven() { + Services.AddScoped(_ => Mock.Of()); BlogPost blogPost = null; var cut = RenderComponent( p => p.Add(c => c.OnBlogPostCreated, bp => blogPost = bp)); @@ -53,6 +57,7 @@ public void ShouldFillGivenBlogPost() .WithTags("tag1", "tag2") .Build(); BlogPost blogPostFromComponent = null; + Services.AddScoped(_ => Mock.Of()); var cut = RenderComponent( p => p.Add(c => c.OnBlogPostCreated, bp => blogPostFromComponent = bp) @@ -74,6 +79,7 @@ public void ShouldFillGivenBlogPost() public void ShouldNotDeleteModelWhenSet() { BlogPost blogPost = null; + Services.AddScoped(_ => Mock.Of()); var cut = RenderComponent( p => p.Add(c => c.ClearAfterCreated, true) .Add(c => c.OnBlogPostCreated, post => blogPost = post)); @@ -94,6 +100,7 @@ public void ShouldNotDeleteModelWhenSet() public void ShouldNotDeleteModelWhenNotSet() { BlogPost blogPost = null; + Services.AddScoped(_ => Mock.Of()); var cut = RenderComponent( p => p.Add(c => c.ClearAfterCreated, false) .Add(c => c.OnBlogPostCreated, post => blogPost = post)); @@ -113,6 +120,7 @@ public void ShouldNotDeleteModelWhenNotSet() [Fact] public void ShouldNotUpdateUpdatedDateWhenCheckboxSet() { + Services.AddScoped(_ => Mock.Of()); var somewhen = new DateTime(1991, 5, 17); var originalBlogPost = new BlogPostBuilder().WithUpdatedDate(somewhen).Build(); BlogPost blogPostFromComponent = null; @@ -135,6 +143,7 @@ public void ShouldNotUpdateUpdatedDateWhenCheckboxSet() [Fact] public void ShouldNotSetOptionToNotUpdateUpdatedDateOnInitialCreate() { + Services.AddScoped(_ => Mock.Of()); var cut = RenderComponent(); var found = cut.FindAll("#updatedate"); @@ -145,6 +154,7 @@ public void ShouldNotSetOptionToNotUpdateUpdatedDateOnInitialCreate() [Fact] public void ShouldAcceptInputWithoutLosingFocusOrEnter() { + Services.AddScoped(_ => Mock.Of()); BlogPost blogPost = null; var cut = RenderComponent( p => p.Add(c => c.OnBlogPostCreated, bp => blogPost = bp)); diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Services/MarkerServiceTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Services/MarkerServiceTests.cs new file mode 100644 index 00000000..9819d33f --- /dev/null +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Services/MarkerServiceTests.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using Bunit; +using FluentAssertions; +using LinkDotNet.Blog.Web.Shared.Services; +using Xunit; + +namespace LinkDotNet.Blog.UnitTests.Web.Shared.Services; + +public class MarkerServiceTests : TestContext +{ + private readonly MarkerService cut; + + public MarkerServiceTests() + { + cut = new MarkerService(JSInterop.JSRuntime); + } + + [Theory] + [InlineData("Test", 0, 2, "**", "**Te**st")] + [InlineData("Test", 0, 4, "**", "**Test**")] + [InlineData("Test", 0, 0, "**", "Test")] + [InlineData("That is a test", 8, 9, "**", "That is **a** test")] + public async Task ShouldMarkString(string source, int startSelect, int endSelect, string fence, string expected) + { + const string element = "id"; + JSInterop.Setup("getSelectionFromElement", element) + .SetResult(new SelectionRange { Start = startSelect, End = endSelect }); + + var actual = await cut.GetNewMarkdownForElementAsync(element, source, fence, fence); + + actual.Should().Be(expected); + } +} \ No newline at end of file From 3c5a20ba85474228e72cb87343d3b770da06c78a Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Tue, 25 Jan 2022 19:51:17 +0100 Subject: [PATCH 3/8] Set Cursor Position --- .../Shared/Services/MarkerService.cs | 3 ++- .../wwwroot/components/selection.js | 6 ++++++ .../Web/Shared/Services/MarkerServiceTests.cs | 17 +++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/LinkDotNet.Blog.Web/Shared/Services/MarkerService.cs b/src/LinkDotNet.Blog.Web/Shared/Services/MarkerService.cs index 0aa8d782..5b9428f9 100644 --- a/src/LinkDotNet.Blog.Web/Shared/Services/MarkerService.cs +++ b/src/LinkDotNet.Blog.Web/Shared/Services/MarkerService.cs @@ -3,7 +3,7 @@ namespace LinkDotNet.Blog.Web.Shared.Services; -public partial class MarkerService : IMarkerService +public class MarkerService : IMarkerService { private readonly IJSRuntime jsRuntime; @@ -23,6 +23,7 @@ public async Task GetNewMarkdownForElementAsync(string elementId, string var beforeMarker = selectionRange.Start > 0 ? content[..selectionRange.Start] : string.Empty; var marker = content[selectionRange.Start..selectionRange.End]; var afterMarker = content[selectionRange.End..]; + await jsRuntime.InvokeVoidAsync("setSelectionFromElement", elementId, selectionRange.Start + fenceBegin.Length); return beforeMarker + fenceBegin + marker + fenceEnd + afterMarker; } } \ No newline at end of file diff --git a/src/LinkDotNet.Blog.Web/wwwroot/components/selection.js b/src/LinkDotNet.Blog.Web/wwwroot/components/selection.js index 05571309..d8218ce0 100644 --- a/src/LinkDotNet.Blog.Web/wwwroot/components/selection.js +++ b/src/LinkDotNet.Blog.Web/wwwroot/components/selection.js @@ -3,4 +3,10 @@ window.getSelectionFromElement = function (id) { const start = elem.selectionStart const end = elem.selectionEnd return { start, end } +} + +window.setSelectionFromElement = function (id, cursor) { + const elem = document.getElementById(id) + elem.selectionStart = cursor + elem.selectionEnd = cursor } \ No newline at end of file diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Services/MarkerServiceTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Services/MarkerServiceTests.cs index 9819d33f..9468db58 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Services/MarkerServiceTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Services/MarkerServiceTests.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Threading.Tasks; using Bunit; using FluentAssertions; @@ -23,6 +24,7 @@ public MarkerServiceTests() public async Task ShouldMarkString(string source, int startSelect, int endSelect, string fence, string expected) { const string element = "id"; + JSInterop.Mode = JSRuntimeMode.Loose; JSInterop.Setup("getSelectionFromElement", element) .SetResult(new SelectionRange { Start = startSelect, End = endSelect }); @@ -30,4 +32,19 @@ public async Task ShouldMarkString(string source, int startSelect, int endSelect actual.Should().Be(expected); } + + [Fact] + public async Task ShouldSetCursorPosition() + { + const string element = "id"; + JSInterop.Mode = JSRuntimeMode.Loose; + JSInterop.Setup("getSelectionFromElement", element) + .SetResult(new SelectionRange { Start = 1, End = 3 }); + + await cut.GetNewMarkdownForElementAsync(element, "Test", "**", "**"); + + var setSelection = JSInterop.Invocations.SingleOrDefault(s => s.Identifier == "setSelectionFromElement"); + setSelection.Arguments.Should().Contain(element); + setSelection.Arguments.Should().Contain(3); + } } \ No newline at end of file From 6c366d53bde2ae6741d9b77a6c0dfe19f0886b09 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Tue, 25 Jan 2022 20:18:23 +0100 Subject: [PATCH 4/8] Added tests for CreateBlogPostPage --- .../Shared/Admin/CreateNewBlogPostTests.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Admin/CreateNewBlogPostTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Admin/CreateNewBlogPostTests.cs index 246fe80f..21f79824 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Admin/CreateNewBlogPostTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Admin/CreateNewBlogPostTests.cs @@ -177,4 +177,28 @@ public void ShouldAcceptInputWithoutLosingFocusOrEnter() blogPost.Tags.Should().HaveCount(3); blogPost.Tags.Select(t => t.Content).Should().Contain(new[] { "Tag1", "Tag2", "Tag3" }); } + + [Theory] + [InlineData("short", "b", true, "**", "**Test**")] + [InlineData("short", "i", true, "*", "*Test*")] + [InlineData("short", "b", false, "**", "Test")] + [InlineData("short", "f", false, "**", "Test")] + [InlineData("content", "b", true, "**", "**Test**")] + [InlineData("content", "i", true, "*", "*Test*")] + [InlineData("content", "b", false, "**", "Test")] + [InlineData("content", "f", false, "**", "Test")] + public void ShouldSetMarkerOnKeyUp(string id, string key, bool ctrlPressed, string fence, string expected) + { + var markerMock = new Mock(); + markerMock.Setup(m => m.GetNewMarkdownForElementAsync(id, "Test", fence, fence)) + .ReturnsAsync(expected); + Services.AddScoped(_ => markerMock.Object); + var cut = RenderComponent(); + cut.Find($"#{id}").Input("Test"); + cut.Find($"#{id}").KeyUp(key, ctrlKey: ctrlPressed); + + var content = cut.Find($"#{id}").TextContent; + + content.Should().Be(expected); + } } \ No newline at end of file From cd654a49984d1233a9e7b82560a41a9c935c7c7c Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Wed, 26 Jan 2022 07:53:52 +0100 Subject: [PATCH 5/8] Fixed behavior of setting cursor after selection --- .../Shared/Admin/CreateNewBlogPost.razor | 6 +++--- src/LinkDotNet.Blog.Web/Shared/Services/MarkerService.cs | 3 ++- .../Web/Shared/Admin/CreateNewBlogPostTests.cs | 2 ++ .../Web/Shared/Services/MarkerServiceTests.cs | 6 +++--- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/LinkDotNet.Blog.Web/Shared/Admin/CreateNewBlogPost.razor b/src/LinkDotNet.Blog.Web/Shared/Admin/CreateNewBlogPost.razor index ac1d197e..9a05a8cc 100644 --- a/src/LinkDotNet.Blog.Web/Shared/Admin/CreateNewBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Shared/Admin/CreateNewBlogPost.razor @@ -19,7 +19,7 @@ - You can use markdown to style your component. + You can use markdown to style your component
@@ -86,8 +86,6 @@ private FeatureInfoDialog FeatureDialog { get; set; } - private ElementReference reference; - private CreateNewModel model = new(); protected override void OnParametersSet() @@ -123,11 +121,13 @@ private async Task MarkShortDescription(KeyboardEventArgs keyboardEventArgs) { model.ShortDescription = await GetNewMarkdownForElementAsync(keyboardEventArgs, model.ShortDescription, "short"); + StateHasChanged(); } private async Task MarkContent(KeyboardEventArgs keyboardEventArgs) { model.Content = await GetNewMarkdownForElementAsync(keyboardEventArgs, model.Content, "content"); + StateHasChanged(); } private async Task GetNewMarkdownForElementAsync(KeyboardEventArgs keyboardEventArgs, string original, string elementId) diff --git a/src/LinkDotNet.Blog.Web/Shared/Services/MarkerService.cs b/src/LinkDotNet.Blog.Web/Shared/Services/MarkerService.cs index 5b9428f9..bbdf2f76 100644 --- a/src/LinkDotNet.Blog.Web/Shared/Services/MarkerService.cs +++ b/src/LinkDotNet.Blog.Web/Shared/Services/MarkerService.cs @@ -23,7 +23,8 @@ public async Task GetNewMarkdownForElementAsync(string elementId, string var beforeMarker = selectionRange.Start > 0 ? content[..selectionRange.Start] : string.Empty; var marker = content[selectionRange.Start..selectionRange.End]; var afterMarker = content[selectionRange.End..]; - await jsRuntime.InvokeVoidAsync("setSelectionFromElement", elementId, selectionRange.Start + fenceBegin.Length); + var shift = selectionRange.End + fenceBegin.Length + fenceEnd.Length; + await jsRuntime.InvokeVoidAsync("setSelectionFromElement", elementId, shift); return beforeMarker + fenceBegin + marker + fenceEnd + afterMarker; } } \ No newline at end of file diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Admin/CreateNewBlogPostTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Admin/CreateNewBlogPostTests.cs index 21f79824..4ecba2ef 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Admin/CreateNewBlogPostTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Admin/CreateNewBlogPostTests.cs @@ -181,10 +181,12 @@ public void ShouldAcceptInputWithoutLosingFocusOrEnter() [Theory] [InlineData("short", "b", true, "**", "**Test**")] [InlineData("short", "i", true, "*", "*Test*")] + [InlineData("short", "h", true, "*", "Test")] [InlineData("short", "b", false, "**", "Test")] [InlineData("short", "f", false, "**", "Test")] [InlineData("content", "b", true, "**", "**Test**")] [InlineData("content", "i", true, "*", "*Test*")] + [InlineData("content", "h", true, "*", "Test")] [InlineData("content", "b", false, "**", "Test")] [InlineData("content", "f", false, "**", "Test")] public void ShouldSetMarkerOnKeyUp(string id, string key, bool ctrlPressed, string fence, string expected) diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Services/MarkerServiceTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Services/MarkerServiceTests.cs index 9468db58..9211064e 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Services/MarkerServiceTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Services/MarkerServiceTests.cs @@ -39,12 +39,12 @@ public async Task ShouldSetCursorPosition() const string element = "id"; JSInterop.Mode = JSRuntimeMode.Loose; JSInterop.Setup("getSelectionFromElement", element) - .SetResult(new SelectionRange { Start = 1, End = 3 }); + .SetResult(new SelectionRange { Start = 2, End = 5 }); - await cut.GetNewMarkdownForElementAsync(element, "Test", "**", "**"); + await cut.GetNewMarkdownForElementAsync(element, "Hello World", "**", "**"); var setSelection = JSInterop.Invocations.SingleOrDefault(s => s.Identifier == "setSelectionFromElement"); setSelection.Arguments.Should().Contain(element); - setSelection.Arguments.Should().Contain(3); + setSelection.Arguments.Should().Contain(9); } } \ No newline at end of file From 011ddbfad30c347f3d18b2c4d7b694ff82b8e9db Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Wed, 26 Jan 2022 19:51:46 +0100 Subject: [PATCH 6/8] Put Shortcuts in own component instead of service --- src/LinkDotNet.Blog.Web/ServiceExtensions.cs | 1 - .../Shared/Admin/CreateNewBlogPost.razor | 32 ++------- .../Shared/{Services => }/SelectionRange.cs | 2 +- .../Shared/Services/IMarkerService.cs | 8 --- .../Shared/Services/MarkerService.cs | 30 -------- .../Shared/TextAreaWithShortcuts.razor | 70 +++++++++++++++++++ .../Pages/Admin/CreateNewBlogPostPageTests.cs | 1 - .../Pages/Admin/UpdateBlogPostPageTests.cs | 2 - .../Shared/Admin/CreateNewBlogPostTests.cs | 36 ---------- .../Web/Shared/Services/MarkerServiceTests.cs | 50 ------------- .../Web/Shared/TextAreaWithShortcutsTests.cs | 55 +++++++++++++++ 11 files changed, 130 insertions(+), 157 deletions(-) rename src/LinkDotNet.Blog.Web/Shared/{Services => }/SelectionRange.cs (69%) delete mode 100644 src/LinkDotNet.Blog.Web/Shared/Services/IMarkerService.cs delete mode 100644 src/LinkDotNet.Blog.Web/Shared/Services/MarkerService.cs create mode 100644 src/LinkDotNet.Blog.Web/Shared/TextAreaWithShortcuts.razor delete mode 100644 tests/LinkDotNet.Blog.UnitTests/Web/Shared/Services/MarkerServiceTests.cs create mode 100644 tests/LinkDotNet.Blog.UnitTests/Web/Shared/TextAreaWithShortcutsTests.cs diff --git a/src/LinkDotNet.Blog.Web/ServiceExtensions.cs b/src/LinkDotNet.Blog.Web/ServiceExtensions.cs index 49261f24..ea3c80bc 100644 --- a/src/LinkDotNet.Blog.Web/ServiceExtensions.cs +++ b/src/LinkDotNet.Blog.Web/ServiceExtensions.cs @@ -16,6 +16,5 @@ public static void RegisterServices(this IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); } } \ No newline at end of file diff --git a/src/LinkDotNet.Blog.Web/Shared/Admin/CreateNewBlogPost.razor b/src/LinkDotNet.Blog.Web/Shared/Admin/CreateNewBlogPost.razor index 9a05a8cc..06f42f3b 100644 --- a/src/LinkDotNet.Blog.Web/Shared/Admin/CreateNewBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Shared/Admin/CreateNewBlogPost.razor @@ -1,7 +1,6 @@ @using LinkDotNet.Blog.Domain @using LinkDotNet.Blog.Web.Shared.Services @inherits MarkdownComponentBase -@inject IMarkerService markerService

@Title

@@ -17,14 +16,14 @@
- + You can use markdown to style your component
- + You can use markdown to style your component. Additional features are listed here Drag and drop markdown files to upload and @@ -117,27 +116,4 @@ { model.Content = content; } - - private async Task MarkShortDescription(KeyboardEventArgs keyboardEventArgs) - { - model.ShortDescription = await GetNewMarkdownForElementAsync(keyboardEventArgs, model.ShortDescription, "short"); - StateHasChanged(); - } - - private async Task MarkContent(KeyboardEventArgs keyboardEventArgs) - { - model.Content = await GetNewMarkdownForElementAsync(keyboardEventArgs, model.Content, "content"); - StateHasChanged(); - } - - private async Task GetNewMarkdownForElementAsync(KeyboardEventArgs keyboardEventArgs, string original, string elementId) - { - return keyboardEventArgs.CtrlKey ? keyboardEventArgs.Key switch - { - "b" => await markerService.GetNewMarkdownForElementAsync(elementId, original, "**", "**"), - "i" => await markerService.GetNewMarkdownForElementAsync(elementId, original, "*", "*"), - _ => original, - } : original; - } - } \ No newline at end of file diff --git a/src/LinkDotNet.Blog.Web/Shared/Services/SelectionRange.cs b/src/LinkDotNet.Blog.Web/Shared/SelectionRange.cs similarity index 69% rename from src/LinkDotNet.Blog.Web/Shared/Services/SelectionRange.cs rename to src/LinkDotNet.Blog.Web/Shared/SelectionRange.cs index 65e488d1..829010c4 100644 --- a/src/LinkDotNet.Blog.Web/Shared/Services/SelectionRange.cs +++ b/src/LinkDotNet.Blog.Web/Shared/SelectionRange.cs @@ -1,4 +1,4 @@ -namespace LinkDotNet.Blog.Web.Shared.Services; +namespace LinkDotNet.Blog.Web.Shared; public sealed class SelectionRange { diff --git a/src/LinkDotNet.Blog.Web/Shared/Services/IMarkerService.cs b/src/LinkDotNet.Blog.Web/Shared/Services/IMarkerService.cs deleted file mode 100644 index 574ad9bf..00000000 --- a/src/LinkDotNet.Blog.Web/Shared/Services/IMarkerService.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Threading.Tasks; - -namespace LinkDotNet.Blog.Web.Shared.Services; - -public interface IMarkerService -{ - Task GetNewMarkdownForElementAsync(string elementId, string content, string fenceBegin, string fenceEnd); -} \ No newline at end of file diff --git a/src/LinkDotNet.Blog.Web/Shared/Services/MarkerService.cs b/src/LinkDotNet.Blog.Web/Shared/Services/MarkerService.cs deleted file mode 100644 index bbdf2f76..00000000 --- a/src/LinkDotNet.Blog.Web/Shared/Services/MarkerService.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.JSInterop; - -namespace LinkDotNet.Blog.Web.Shared.Services; - -public class MarkerService : IMarkerService -{ - private readonly IJSRuntime jsRuntime; - - public MarkerService(IJSRuntime jsRuntime) - { - this.jsRuntime = jsRuntime; - } - - public async Task GetNewMarkdownForElementAsync(string elementId, string content, string fenceBegin, string fenceEnd) - { - var selectionRange = await jsRuntime.InvokeAsync("getSelectionFromElement", elementId); - if (selectionRange.Start == selectionRange.End) - { - return content; - } - - var beforeMarker = selectionRange.Start > 0 ? content[..selectionRange.Start] : string.Empty; - var marker = content[selectionRange.Start..selectionRange.End]; - var afterMarker = content[selectionRange.End..]; - var shift = selectionRange.End + fenceBegin.Length + fenceEnd.Length; - await jsRuntime.InvokeVoidAsync("setSelectionFromElement", elementId, shift); - return beforeMarker + fenceBegin + marker + fenceEnd + afterMarker; - } -} \ No newline at end of file diff --git a/src/LinkDotNet.Blog.Web/Shared/TextAreaWithShortcuts.razor b/src/LinkDotNet.Blog.Web/Shared/TextAreaWithShortcuts.razor new file mode 100644 index 00000000..c4a914ae --- /dev/null +++ b/src/LinkDotNet.Blog.Web/Shared/TextAreaWithShortcuts.razor @@ -0,0 +1,70 @@ +@inject IJSRuntime jsRuntime + + + +@code { + private string textContent = string.Empty; + + [Parameter] + public string Value + { + get => textContent; + set + { + if (textContent != value) + { + textContent = value; + ValueChanged.InvokeAsync(value); + } + } + } + + [Parameter] + public EventCallback ValueChanged { get; set; } + + [Parameter] + public string Class { get; set; } + + [Parameter] + public string Id { get; set; } + + [Parameter] + public int Rows { get; set; } + + private async Task MarkShortDescription(KeyboardEventArgs keyboardEventArgs) + { + Value = await GetNewMarkdownForElementAsync(keyboardEventArgs, Value, Id); + StateHasChanged(); + } + + private async Task GetNewMarkdownForElementAsync(KeyboardEventArgs keyboardEventArgs, string original, string elementId) + { + return keyboardEventArgs.CtrlKey ? keyboardEventArgs.Key switch + { + "b" => await GetNewMarkdownForElementAsync(elementId, original, "**", "**"), + "i" => await GetNewMarkdownForElementAsync(elementId, original, "*", "*"), + _ => original, + } : original; + } + + private async Task GetNewMarkdownForElementAsync( + string elementId, + string content, + string fenceBegin, + string fenceEnd) + { + var selectionRange = await jsRuntime.InvokeAsync("getSelectionFromElement", elementId); + if (selectionRange.Start == selectionRange.End) + { + return content; + } + + var beforeMarker = selectionRange.Start > 0 ? content[..selectionRange.Start] : string.Empty; + var marker = content[selectionRange.Start..selectionRange.End]; + var afterMarker = content[selectionRange.End..]; + var shift = selectionRange.End + fenceBegin.Length + fenceEnd.Length; + await jsRuntime.InvokeVoidAsync("setSelectionFromElement", elementId, shift); + return beforeMarker + fenceBegin + marker + fenceEnd + afterMarker; + } +} \ No newline at end of file diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Pages/Admin/CreateNewBlogPostPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Pages/Admin/CreateNewBlogPostPageTests.cs index 669b8ea3..b1ed36b4 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Pages/Admin/CreateNewBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Pages/Admin/CreateNewBlogPostPageTests.cs @@ -28,7 +28,6 @@ public async Task ShouldSaveBlogPostOnSave() ctx.Services.AddScoped(_ => toastService.Object); ctx.ComponentFactories.AddStub(); ctx.Services.AddScoped(_ => Mock.Of()); - ctx.Services.AddScoped(_ => Mock.Of()); using var cut = ctx.RenderComponent(); var newBlogPost = cut.FindComponent(); diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Pages/Admin/UpdateBlogPostPageTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Pages/Admin/UpdateBlogPostPageTests.cs index 7179b2a3..4c70af46 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Pages/Admin/UpdateBlogPostPageTests.cs +++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Pages/Admin/UpdateBlogPostPageTests.cs @@ -10,7 +10,6 @@ using LinkDotNet.Blog.Web.Pages.Admin; using LinkDotNet.Blog.Web.Shared; using LinkDotNet.Blog.Web.Shared.Admin; -using LinkDotNet.Blog.Web.Shared.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Moq; @@ -31,7 +30,6 @@ public async Task ShouldSaveBlogPostOnSave() ctx.Services.AddScoped>(_ => Repository); ctx.Services.AddScoped(_ => toastService.Object); ctx.ComponentFactories.AddStub(); - ctx.Services.AddScoped(_ => Mock.Of()); using var cut = ctx.RenderComponent( p => p.Add(s => s.BlogPostId, blogPost.Id)); var newBlogPost = cut.FindComponent(); diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Admin/CreateNewBlogPostTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Admin/CreateNewBlogPostTests.cs index 4ecba2ef..1e6c44b9 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Admin/CreateNewBlogPostTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Admin/CreateNewBlogPostTests.cs @@ -6,9 +6,6 @@ using LinkDotNet.Blog.TestUtilities; using LinkDotNet.Blog.Web.Shared; using LinkDotNet.Blog.Web.Shared.Admin; -using LinkDotNet.Blog.Web.Shared.Services; -using Microsoft.Extensions.DependencyInjection; -using Moq; using Xunit; namespace LinkDotNet.Blog.UnitTests.Web.Shared.Admin; @@ -23,7 +20,6 @@ public CreateNewBlogPostTests() [Fact] public void ShouldCreateNewBlogPostWhenValidDataGiven() { - Services.AddScoped(_ => Mock.Of()); BlogPost blogPost = null; var cut = RenderComponent( p => p.Add(c => c.OnBlogPostCreated, bp => blogPost = bp)); @@ -57,7 +53,6 @@ public void ShouldFillGivenBlogPost() .WithTags("tag1", "tag2") .Build(); BlogPost blogPostFromComponent = null; - Services.AddScoped(_ => Mock.Of()); var cut = RenderComponent( p => p.Add(c => c.OnBlogPostCreated, bp => blogPostFromComponent = bp) @@ -79,7 +74,6 @@ public void ShouldFillGivenBlogPost() public void ShouldNotDeleteModelWhenSet() { BlogPost blogPost = null; - Services.AddScoped(_ => Mock.Of()); var cut = RenderComponent( p => p.Add(c => c.ClearAfterCreated, true) .Add(c => c.OnBlogPostCreated, post => blogPost = post)); @@ -100,7 +94,6 @@ public void ShouldNotDeleteModelWhenSet() public void ShouldNotDeleteModelWhenNotSet() { BlogPost blogPost = null; - Services.AddScoped(_ => Mock.Of()); var cut = RenderComponent( p => p.Add(c => c.ClearAfterCreated, false) .Add(c => c.OnBlogPostCreated, post => blogPost = post)); @@ -120,7 +113,6 @@ public void ShouldNotDeleteModelWhenNotSet() [Fact] public void ShouldNotUpdateUpdatedDateWhenCheckboxSet() { - Services.AddScoped(_ => Mock.Of()); var somewhen = new DateTime(1991, 5, 17); var originalBlogPost = new BlogPostBuilder().WithUpdatedDate(somewhen).Build(); BlogPost blogPostFromComponent = null; @@ -143,7 +135,6 @@ public void ShouldNotUpdateUpdatedDateWhenCheckboxSet() [Fact] public void ShouldNotSetOptionToNotUpdateUpdatedDateOnInitialCreate() { - Services.AddScoped(_ => Mock.Of()); var cut = RenderComponent(); var found = cut.FindAll("#updatedate"); @@ -154,7 +145,6 @@ public void ShouldNotSetOptionToNotUpdateUpdatedDateOnInitialCreate() [Fact] public void ShouldAcceptInputWithoutLosingFocusOrEnter() { - Services.AddScoped(_ => Mock.Of()); BlogPost blogPost = null; var cut = RenderComponent( p => p.Add(c => c.OnBlogPostCreated, bp => blogPost = bp)); @@ -177,30 +167,4 @@ public void ShouldAcceptInputWithoutLosingFocusOrEnter() blogPost.Tags.Should().HaveCount(3); blogPost.Tags.Select(t => t.Content).Should().Contain(new[] { "Tag1", "Tag2", "Tag3" }); } - - [Theory] - [InlineData("short", "b", true, "**", "**Test**")] - [InlineData("short", "i", true, "*", "*Test*")] - [InlineData("short", "h", true, "*", "Test")] - [InlineData("short", "b", false, "**", "Test")] - [InlineData("short", "f", false, "**", "Test")] - [InlineData("content", "b", true, "**", "**Test**")] - [InlineData("content", "i", true, "*", "*Test*")] - [InlineData("content", "h", true, "*", "Test")] - [InlineData("content", "b", false, "**", "Test")] - [InlineData("content", "f", false, "**", "Test")] - public void ShouldSetMarkerOnKeyUp(string id, string key, bool ctrlPressed, string fence, string expected) - { - var markerMock = new Mock(); - markerMock.Setup(m => m.GetNewMarkdownForElementAsync(id, "Test", fence, fence)) - .ReturnsAsync(expected); - Services.AddScoped(_ => markerMock.Object); - var cut = RenderComponent(); - cut.Find($"#{id}").Input("Test"); - cut.Find($"#{id}").KeyUp(key, ctrlKey: ctrlPressed); - - var content = cut.Find($"#{id}").TextContent; - - content.Should().Be(expected); - } } \ No newline at end of file diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Services/MarkerServiceTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Services/MarkerServiceTests.cs deleted file mode 100644 index 9211064e..00000000 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Shared/Services/MarkerServiceTests.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using Bunit; -using FluentAssertions; -using LinkDotNet.Blog.Web.Shared.Services; -using Xunit; - -namespace LinkDotNet.Blog.UnitTests.Web.Shared.Services; - -public class MarkerServiceTests : TestContext -{ - private readonly MarkerService cut; - - public MarkerServiceTests() - { - cut = new MarkerService(JSInterop.JSRuntime); - } - - [Theory] - [InlineData("Test", 0, 2, "**", "**Te**st")] - [InlineData("Test", 0, 4, "**", "**Test**")] - [InlineData("Test", 0, 0, "**", "Test")] - [InlineData("That is a test", 8, 9, "**", "That is **a** test")] - public async Task ShouldMarkString(string source, int startSelect, int endSelect, string fence, string expected) - { - const string element = "id"; - JSInterop.Mode = JSRuntimeMode.Loose; - JSInterop.Setup("getSelectionFromElement", element) - .SetResult(new SelectionRange { Start = startSelect, End = endSelect }); - - var actual = await cut.GetNewMarkdownForElementAsync(element, source, fence, fence); - - actual.Should().Be(expected); - } - - [Fact] - public async Task ShouldSetCursorPosition() - { - const string element = "id"; - JSInterop.Mode = JSRuntimeMode.Loose; - JSInterop.Setup("getSelectionFromElement", element) - .SetResult(new SelectionRange { Start = 2, End = 5 }); - - await cut.GetNewMarkdownForElementAsync(element, "Hello World", "**", "**"); - - var setSelection = JSInterop.Invocations.SingleOrDefault(s => s.Identifier == "setSelectionFromElement"); - setSelection.Arguments.Should().Contain(element); - setSelection.Arguments.Should().Contain(9); - } -} \ No newline at end of file diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Shared/TextAreaWithShortcutsTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Shared/TextAreaWithShortcutsTests.cs new file mode 100644 index 00000000..bd5e016d --- /dev/null +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Shared/TextAreaWithShortcutsTests.cs @@ -0,0 +1,55 @@ +using System.Linq; +using Bunit; +using FluentAssertions; +using LinkDotNet.Blog.Web.Shared; +using Xunit; + +namespace LinkDotNet.Blog.UnitTests.Web.Shared; + +public class TextAreaWithShortcutsTests : TestContext +{ + [Theory] + [InlineData("b", 0, 4, true, "**Test**")] + [InlineData("i", 0, 4, true, "*Test*")] + [InlineData("h", 0, 1, true, "Test")] + [InlineData("b", 0, 1, false, "Test")] + [InlineData("f", 0, 4, false, "Test")] + [InlineData("b", 0, 0, true, "Test")] + public void ShouldSetMarkerOnKeyUp(string key, int start, int end, bool ctrlPressed, string expected) + { + const string id = "id"; + var range = new SelectionRange + { + Start = start, + End = end, + }; + JSInterop.Mode = JSRuntimeMode.Loose; + JSInterop.Setup("getSelectionFromElement", id).SetResult(range); + var cut = RenderComponent( + p => p.Add(s => s.Id, id)); + cut.Find("textarea").Input("Test"); + cut.Find("textarea").KeyUp(key, ctrlKey: ctrlPressed); + + var content = cut.Find("textarea").TextContent; + + content.Should().Be(expected); + } + + [Fact] + public void ShouldSetCursorPosition() + { + const string element = "id"; + JSInterop.Mode = JSRuntimeMode.Loose; + JSInterop.Setup("getSelectionFromElement", element) + .SetResult(new SelectionRange { Start = 2, End = 5 }); + var cut = RenderComponent( + p => p.Add(s => s.Id, element)); + cut.Find($"#{element}").Input("Hello World"); + + cut.Find($"#{element}").KeyUp("b", ctrlKey: true); + + var setSelection = JSInterop.Invocations.SingleOrDefault(s => s.Identifier == "setSelectionFromElement"); + setSelection.Arguments.Should().Contain(element); + setSelection.Arguments.Should().Contain(9); + } +} \ No newline at end of file From 8ba849c36790f95241a5756e0aea66bd36db97cf Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Wed, 26 Jan 2022 20:18:50 +0100 Subject: [PATCH 7/8] Added link --- .../Shared/Admin/CreateNewBlogPost.razor | 2 +- .../Shared/Admin/FeatureInfoDialog.razor | 24 +++++++++++++++++++ .../Shared/TextAreaWithShortcuts.razor | 6 +++-- .../Web/Shared/TextAreaWithShortcutsTests.cs | 1 + 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/LinkDotNet.Blog.Web/Shared/Admin/CreateNewBlogPost.razor b/src/LinkDotNet.Blog.Web/Shared/Admin/CreateNewBlogPost.razor index 06f42f3b..8a1dbfad 100644 --- a/src/LinkDotNet.Blog.Web/Shared/Admin/CreateNewBlogPost.razor +++ b/src/LinkDotNet.Blog.Web/Shared/Admin/CreateNewBlogPost.razor @@ -24,7 +24,7 @@ - You can use markdown to style your component. Additional features are listed here + You can use markdown to style your component. Additional features and keyboard shortcuts are listed here Drag and drop markdown files to upload and insert them diff --git a/src/LinkDotNet.Blog.Web/Shared/Admin/FeatureInfoDialog.razor b/src/LinkDotNet.Blog.Web/Shared/Admin/FeatureInfoDialog.razor index f4bbf7fd..4e9b83f9 100644 --- a/src/LinkDotNet.Blog.Web/Shared/Admin/FeatureInfoDialog.razor +++ b/src/LinkDotNet.Blog.Web/Shared/Admin/FeatureInfoDialog.razor @@ -3,6 +3,30 @@

Features marked with are experimental and can change heavily, get removed or the usage changes.

Use with caution and check the changelog


+

Shortcuts

+ + + + + + + + + + + + + + + + + + + + + +
Keyboard shortcutDescription
control bInserts Markdown formatting for bolding text
control iInserts Markdown formatting for italicizing text
control mInserts Markdown formatting for creating a link
+

Slide-Show

Will create a slide-show with images specified by the tag.

Usage: diff --git a/src/LinkDotNet.Blog.Web/Shared/TextAreaWithShortcuts.razor b/src/LinkDotNet.Blog.Web/Shared/TextAreaWithShortcuts.razor index c4a914ae..63105035 100644 --- a/src/LinkDotNet.Blog.Web/Shared/TextAreaWithShortcuts.razor +++ b/src/LinkDotNet.Blog.Web/Shared/TextAreaWithShortcuts.razor @@ -1,7 +1,8 @@ @inject IJSRuntime jsRuntime - + @code { private string textContent = string.Empty; @@ -44,6 +45,7 @@ { "b" => await GetNewMarkdownForElementAsync(elementId, original, "**", "**"), "i" => await GetNewMarkdownForElementAsync(elementId, original, "*", "*"), + "m" => await GetNewMarkdownForElementAsync(elementId, original, "[","]()"), _ => original, } : original; } diff --git a/tests/LinkDotNet.Blog.UnitTests/Web/Shared/TextAreaWithShortcutsTests.cs b/tests/LinkDotNet.Blog.UnitTests/Web/Shared/TextAreaWithShortcutsTests.cs index bd5e016d..8742ae1e 100644 --- a/tests/LinkDotNet.Blog.UnitTests/Web/Shared/TextAreaWithShortcutsTests.cs +++ b/tests/LinkDotNet.Blog.UnitTests/Web/Shared/TextAreaWithShortcutsTests.cs @@ -11,6 +11,7 @@ public class TextAreaWithShortcutsTests : TestContext [Theory] [InlineData("b", 0, 4, true, "**Test**")] [InlineData("i", 0, 4, true, "*Test*")] + [InlineData("m", 0, 4, true, "[Test]()")] [InlineData("h", 0, 1, true, "Test")] [InlineData("b", 0, 1, false, "Test")] [InlineData("f", 0, 4, false, "Test")] From 5a8f801da6911922bfc79540705ad1a5f37b96b1 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Wed, 26 Jan 2022 20:38:15 +0100 Subject: [PATCH 8/8] Refactored how fences are applied --- .../Shared/TextAreaWithShortcuts.razor | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/LinkDotNet.Blog.Web/Shared/TextAreaWithShortcuts.razor b/src/LinkDotNet.Blog.Web/Shared/TextAreaWithShortcuts.razor index 63105035..57f44bef 100644 --- a/src/LinkDotNet.Blog.Web/Shared/TextAreaWithShortcuts.razor +++ b/src/LinkDotNet.Blog.Web/Shared/TextAreaWithShortcuts.razor @@ -6,6 +6,8 @@ @code { private string textContent = string.Empty; + private const string SelectedMarker = "#selection#"; + private const string CursorMarker = "#cursor#"; [Parameter] public string Value @@ -39,13 +41,16 @@ StateHasChanged(); } - private async Task GetNewMarkdownForElementAsync(KeyboardEventArgs keyboardEventArgs, string original, string elementId) + private async Task GetNewMarkdownForElementAsync( + KeyboardEventArgs keyboardEventArgs, + string original, + string elementId) { return keyboardEventArgs.CtrlKey ? keyboardEventArgs.Key switch { - "b" => await GetNewMarkdownForElementAsync(elementId, original, "**", "**"), - "i" => await GetNewMarkdownForElementAsync(elementId, original, "*", "*"), - "m" => await GetNewMarkdownForElementAsync(elementId, original, "[","]()"), + "b" => await GetNewMarkdownForElementAsync(elementId, original, $"**{SelectedMarker}**{CursorMarker}"), + "i" => await GetNewMarkdownForElementAsync(elementId, original, $"*{SelectedMarker}*{CursorMarker}"), + "m" => await GetNewMarkdownForElementAsync(elementId, original, $"[{SelectedMarker}]({CursorMarker})"), _ => original, } : original; } @@ -53,8 +58,7 @@ private async Task GetNewMarkdownForElementAsync( string elementId, string content, - string fenceBegin, - string fenceEnd) + string fence) { var selectionRange = await jsRuntime.InvokeAsync("getSelectionFromElement", elementId); if (selectionRange.Start == selectionRange.End) @@ -63,10 +67,12 @@ } var beforeMarker = selectionRange.Start > 0 ? content[..selectionRange.Start] : string.Empty; - var marker = content[selectionRange.Start..selectionRange.End]; + var selectedContent = content[selectionRange.Start..selectionRange.End]; + var fencedContent = fence.Replace(SelectedMarker, selectedContent); var afterMarker = content[selectionRange.End..]; - var shift = selectionRange.End + fenceBegin.Length + fenceEnd.Length; + var shift = selectionRange.Start + fencedContent.IndexOf(CursorMarker, StringComparison.Ordinal); + var removedCursor = fencedContent.Replace(CursorMarker, string.Empty); await jsRuntime.InvokeVoidAsync("setSelectionFromElement", elementId, shift); - return beforeMarker + fenceBegin + marker + fenceEnd + afterMarker; + return beforeMarker + removedCursor + afterMarker; } } \ No newline at end of file