@using System.IO @using System.Linq @using LinkDotNet.Blog.Web.Features.Services.FileUpload @using IToastService = Blazored.Toast.Services.IToastService @inject IBlobUploadService BlobUploadService @inject IToastService ToastService @inject IJSRuntime JSRuntime @implements IAsyncDisposable
@if (showTablePicker) {
Insert Table
}
@if (viewMode == ViewMode.Preview) {
@((MarkupString)previewHtml)
} else { }
@code { private enum ViewMode { Editor, Preview } public sealed class UndoRedoState { public bool CanUndo { get; set; } public bool CanRedo { get; set; } } private string Height => $"{Rows * 25}px"; private ElementReference textAreaRef; private InputFile inputFileRef = default!; private UploadFileModalDialog UploadDialog { get; set; } = default!; private ViewMode viewMode = ViewMode.Editor; private string previewHtml = string.Empty; private DotNetObjectReference? dotNetHelper; private bool isMac; private bool canUndo; private bool canRedo; private bool showTablePicker; private int tableCols = 2; private int tableRows = 2; private const long MaxFileSizeBytes = 512 * 1024; // 512 KB #pragma warning disable BL0007 [Parameter] public string Value { get; set { if (field != value) { field = value; _ = InvokeAsync(async () => { await ValueChanged.InvokeAsync(value); await UpdatePreview(); }); } } } = string.Empty; #pragma warning restore [Parameter] public EventCallback ValueChanged { get; set; } [Parameter] public string Class { get; set; } = string.Empty; [Parameter] public string Id { get; set; } = string.Empty; [Parameter] public int Rows { get; set; } [Parameter] public string Placeholder { get; set; } = string.Empty; [Parameter] public Func> PreviewFunction { get; set; } = s => Task.FromResult(MarkdownConverter.ToMarkupString(s).Value); protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { try { isMac = await JSRuntime.InvokeAsync("markdownEditor.isMac"); dotNetHelper = DotNetObjectReference.Create(this); } catch (JSException) { canUndo = false; canRedo = false; return; } } if (viewMode == ViewMode.Editor && dotNetHelper is not null) { try { await JSRuntime.InvokeVoidAsync("markdownEditor.setupKeyboardShortcuts", textAreaRef, dotNetHelper); } catch (JSException) { // Keyboard shortcuts unavailable; toolbar buttons remain functional } } } [JSInvokable] public async Task HandleKeyboardShortcut(string action) { switch (action) { case "bold": await InsertBold(); break; case "italic": await InsertItalic(); break; case "code": await InsertCode(); break; case "link": await InsertLink(); break; case "quote": await InsertQuote(); break; case "hr": await InsertHorizontalRule(); break; case "preview": await TogglePreview(); break; } } private string GetShortcutText(string action, string key) { var modifier = isMac ? "⌘" : "Ctrl"; return $"{action} ({modifier}+{key})"; } private async Task OnInputAsync() { await UpdateUndoRedoState(); } private async Task UpdatePreview() { if (viewMode == ViewMode.Preview) { previewHtml = await PreviewFunction(Value); StateHasChanged(); await InvokeAsync(async () => { await JSRuntime.InvokeVoidAsync("markdownEditor.highlightCodeBlocks"); }); } } private async Task TogglePreview() { viewMode = viewMode == ViewMode.Editor ? ViewMode.Preview : ViewMode.Editor; if (viewMode == ViewMode.Preview) { await UpdatePreview(); } } private async Task UpdateUndoRedoState() { try { var state = await JSRuntime.InvokeAsync("markdownEditor.getUndoRedoState", textAreaRef); canUndo = state.CanUndo; canRedo = state.CanRedo; } catch { canUndo = !string.IsNullOrEmpty(Value); canRedo = false; } } private async Task Undo() { await JSRuntime.InvokeVoidAsync("markdownEditor.undo", textAreaRef); await UpdateUndoRedoState(); } private async Task Redo() { await JSRuntime.InvokeVoidAsync("markdownEditor.redo", textAreaRef); await UpdateUndoRedoState(); } private Task InsertBold() => InsertMarkdown("**", "**"); private Task InsertItalic() => InsertMarkdown("*", "*"); private Task InsertH1() => InsertMarkdown("# ", ""); private Task InsertH2() => InsertMarkdown("## ", ""); private Task InsertH3() => InsertMarkdown("### ", ""); private Task InsertQuote() => InsertLinePrefixMarkdown("> "); private Task InsertCode() => InsertMarkdown("`", "`"); private Task InsertCodeBlock() => InsertMarkdown("```\n", "\n```"); private Task InsertLink() => InsertMarkdown("[", "](url)"); private Task InsertUnorderedList() => InsertMarkdown("- ", ""); private Task InsertOrderedList() => InsertMarkdown("1. ", ""); private Task InsertStrikethrough() => InsertMarkdown("~~", "~~"); private Task InsertSuperscript() => InsertMarkdown("", ""); private Task InsertSubscript() => InsertMarkdown("", ""); private Task InsertTaskList() => InsertMarkdown("- [ ] ", ""); private void ToggleTablePicker() => showTablePicker = !showTablePicker; private async Task InsertLinePrefixMarkdown(string prefix) { try { await JSRuntime.InvokeVoidAsync("markdownEditor.insertLinePrefixes", textAreaRef, prefix); await textAreaRef.FocusAsync(); } catch { Value += prefix; } } private async Task InsertHorizontalRule() { try { await JSRuntime.InvokeVoidAsync("markdownEditor.insertHorizontalRule", textAreaRef); await textAreaRef.FocusAsync(); } catch { Value += "\n\n---\n\n"; } } private async Task InsertTable(int cols, int rows) { showTablePicker = false; var emptyRow = "| " + string.Join(" | ", Enumerable.Repeat("", cols)) + " |"; var separator = "| " + string.Join(" | ", Enumerable.Repeat("---", cols)) + " |"; var table = string.Join("\n", new[] { emptyRow, separator }.Concat(Enumerable.Repeat(emptyRow, rows))); await InsertMarkdown(table, ""); } private async Task InsertMarkdown(string prefix, string suffix) { try { await JSRuntime.InvokeVoidAsync("markdownEditor.insertText", textAreaRef, prefix, suffix); await textAreaRef.FocusAsync(); } catch { Value += prefix + suffix; } } private async Task ShowImageUpload() { await JSRuntime.InvokeVoidAsync("markdownEditor.clickInputFile", inputFileRef.Element); } public async ValueTask DisposeAsync() { dotNetHelper?.Dispose(); } private async Task HandleFileSelected(InputFileChangeEventArgs e) { foreach (var file in e.GetMultipleFiles()) { try { if (file.Size > MaxFileSizeBytes) { ToastService.ShowError($"File '{file.Name}' exceeds the maximum allowed size of 512 KB."); continue; } using var memoryStream = new MemoryStream(); await file.OpenReadStream(maxAllowedSize: MaxFileSizeBytes).CopyToAsync(memoryStream); memoryStream.Position = 0; var options = await UploadDialog.ShowAsync(file.Name); if (options is null) { continue; } var url = await BlobUploadService.UploadFileAsync(options.Name, memoryStream, new UploadOptions { SetCacheControlHeader = options.CacheMedia, }); await InsertMarkdown($"![{file.Name}]({url})", ""); ToastService.ShowSuccess($"Successfully uploaded {file.Name}"); } catch (Exception ex) { ToastService.ShowError($"Error while uploading file: {ex.Message}"); } } } }