From 84b31800ad72c7d18ef38b47230f4d53637ed4c8 Mon Sep 17 00:00:00 2001 From: James Kolpack Date: Sat, 17 Jan 2026 11:33:57 -0500 Subject: [PATCH] Add MarkdownTablePasteService and integrate with NoteEditDialog and PageNoteDialog components This commit introduces the MarkdownTablePasteService to facilitate markdown table pasting functionality. The service is registered in Program.cs and injected into NoteEditDialog and PageNoteDialog components. Additionally, OnAfterRenderAsync lifecycle methods are implemented in both dialog components to initialize the service after the editor is rendered, enhancing the user experience for note editing. This change supports improved markdown handling within the application. --- WebApp/Components/App.razor | 1 + WebApp/Components/Pages/NoteEditDialog.razor | 13 + .../Shared/Components/PageNoteDialog.razor | 29 ++- WebApp/Program.cs | 1 + WebApp/Services/MarkdownTablePasteService.cs | 41 ++++ WebApp/wwwroot/js/markdownTablePaste.js | 229 ++++++++++++++++++ 6 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 WebApp/Services/MarkdownTablePasteService.cs create mode 100644 WebApp/wwwroot/js/markdownTablePaste.js diff --git a/WebApp/Components/App.razor b/WebApp/Components/App.razor index 2445bcf..32271cd 100644 --- a/WebApp/Components/App.razor +++ b/WebApp/Components/App.razor @@ -25,6 +25,7 @@ + diff --git a/WebApp/Components/Pages/NoteEditDialog.razor b/WebApp/Components/Pages/NoteEditDialog.razor index 83e2294..7b1ea86 100644 --- a/WebApp/Components/Pages/NoteEditDialog.razor +++ b/WebApp/Components/Pages/NoteEditDialog.razor @@ -5,6 +5,7 @@ @inject INotesService NotesService @inject ISnackbar Snackbar @inject IDialogService DialogService +@inject MarkdownTablePasteService MarkdownTablePasteService @@ -42,6 +43,7 @@ public bool IsEdit { get; set; } private Note _note = null!; + private bool _pasteMarkdownInitialized = false; private bool IsPageNote => Note?.Title?.StartsWith("@") ?? false; @@ -55,6 +57,17 @@ }; } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && !_pasteMarkdownInitialized) + { + // Initialize paste-markdown after EasyMDE has rendered + await Task.Delay(150); // Wait for EasyMDE to initialize + await MarkdownTablePasteService.InitializeAsync(); + _pasteMarkdownInitialized = true; + } + } + private async Task Save() { if (string.IsNullOrWhiteSpace(_note.Title)) diff --git a/WebApp/Components/Shared/Components/PageNoteDialog.razor b/WebApp/Components/Shared/Components/PageNoteDialog.razor index 81c7916..4965257 100644 --- a/WebApp/Components/Shared/Components/PageNoteDialog.razor +++ b/WebApp/Components/Shared/Components/PageNoteDialog.razor @@ -1,6 +1,7 @@ @namespace WebApp.Components.Shared.Components @inject INotesService NotesService @inject ISnackbar Snackbar +@inject MarkdownTablePasteService MarkdownTablePasteService @implements IAsyncDisposable @@ -77,6 +78,7 @@ private bool _isEditMode = false; private CancellationTokenSource? _cancellationTokenSource; private bool _isDisposed = false; + private bool _pasteMarkdownInitialized = false; protected override void OnInitialized() { @@ -94,6 +96,20 @@ await LoadPageNote(); } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && _isEditMode && !_pasteMarkdownInitialized) + { + // Initialize paste-markdown after first render if already in edit mode + await Task.Delay(150); // Wait for EasyMDE to initialize + if (!_isDisposed) + { + await MarkdownTablePasteService.InitializeAsync(); + _pasteMarkdownInitialized = true; + } + } + } + private async Task LoadPageNote() { if (_isDisposed) return; @@ -201,11 +217,22 @@ } } - private void EnterEditMode() + private async Task EnterEditMode() { if (_isDisposed) return; _isEditMode = true; StateHasChanged(); + + // Initialize paste-markdown after editor is rendered + if (!_pasteMarkdownInitialized) + { + await Task.Delay(150); // Wait for EasyMDE to initialize + if (!_isDisposed) + { + await MarkdownTablePasteService.InitializeAsync(); + _pasteMarkdownInitialized = true; + } + } } private void Cancel() diff --git a/WebApp/Program.cs b/WebApp/Program.cs index a30f86b..5276730 100644 --- a/WebApp/Program.cs +++ b/WebApp/Program.cs @@ -194,6 +194,7 @@ builder.Services.AddScoped(sp => builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // State container for maintaining state per user connection (Blazor Server) builder.Services.AddScoped(); diff --git a/WebApp/Services/MarkdownTablePasteService.cs b/WebApp/Services/MarkdownTablePasteService.cs new file mode 100644 index 0000000..c7c918d --- /dev/null +++ b/WebApp/Services/MarkdownTablePasteService.cs @@ -0,0 +1,41 @@ +using Microsoft.JSInterop; + +namespace WebApp.Services; + +/// +/// Service for initializing paste-markdown functionality in MarkdownEditor components. +/// Enables automatic conversion of pasted spreadsheet tables (Google Sheets, Excel) to Markdown tables. +/// +public class MarkdownTablePasteService +{ + private readonly IJSRuntime _jsRuntime; + private readonly ILogger _logger; + + public MarkdownTablePasteService(IJSRuntime jsRuntime, ILogger logger) + { + _jsRuntime = jsRuntime; + _logger = logger; + } + + /// + /// Initializes paste-markdown for a MarkdownEditor instance. + /// Should be called after the editor has been rendered and EasyMDE has initialized. + /// + /// Optional ID of the editor wrapper element. If null, will attempt to find the most recent editor. + /// A task that represents the asynchronous operation. + public async Task InitializeAsync(string? editorId = null) + { + try + { + await _jsRuntime.InvokeVoidAsync("markdownTablePaste.initialize", editorId); + } + catch (JSException ex) + { + _logger.LogWarning(ex, "Failed to initialize paste-markdown for editor {EditorId}", editorId ?? "unknown"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error initializing paste-markdown for editor {EditorId}", editorId ?? "unknown"); + } + } +} diff --git a/WebApp/wwwroot/js/markdownTablePaste.js b/WebApp/wwwroot/js/markdownTablePaste.js new file mode 100644 index 0000000..5313626 --- /dev/null +++ b/WebApp/wwwroot/js/markdownTablePaste.js @@ -0,0 +1,229 @@ +// Integration for converting pasted spreadsheet tables to Markdown tables in EasyMDE/CodeMirror +// Handles both HTML tables (from Google Sheets) and tab-separated values + +window.markdownTablePaste = { + /** + * Initializes paste handler for a MarkdownEditor instance. + * Finds the textarea or CodeMirror instance created by EasyMDE and attaches paste event handler. + * @param {string} editorId - The ID of the editor wrapper element, or null to find by class + */ + initialize: function(editorId) { + let attempts = 0; + const maxAttempts = 5; + + const tryInitialize = function() { + attempts++; + + let textarea = null; + let codeMirror = null; + + // Try to find the textarea element + if (editorId) { + const editorElement = document.getElementById(editorId); + if (editorElement) { + textarea = editorElement.querySelector('textarea'); + } + } else { + // Find the most recently created EasyMDE textarea (last one in DOM) + const containers = document.querySelectorAll('.EasyMDEContainer'); + if (containers.length > 0) { + const lastContainer = containers[containers.length - 1]; + textarea = lastContainer.querySelector('textarea'); + } + + // Fallback: try to find any textarea near an editor-toolbar + if (!textarea) { + const toolbars = document.querySelectorAll('.editor-toolbar'); + if (toolbars.length > 0) { + const lastToolbar = toolbars[toolbars.length - 1]; + const nextSibling = lastToolbar.nextElementSibling; + if (nextSibling && nextSibling.tagName === 'TEXTAREA') { + textarea = nextSibling; + } + } + } + + // Another fallback: find any textarea that's a child of a container with editor classes + if (!textarea) { + const allTextareas = document.querySelectorAll('textarea'); + for (let ta of allTextareas) { + const parent = ta.parentElement; + if (parent && ( + parent.classList.contains('EasyMDEContainer') || + parent.classList.contains('editor') || + parent.querySelector('.editor-toolbar') + )) { + textarea = ta; + break; + } + } + } + } + + // If we found a textarea, try to get the CodeMirror instance from EasyMDE + if (textarea) { + if (window.EasyMDE) { + const easyMDEInstances = window.EasyMDE.instances || []; + + for (let i = 0; i < easyMDEInstances.length; i++) { + const instance = easyMDEInstances[i]; + if (instance && instance.codemirror) { + const cmTextarea = instance.codemirror.getTextArea(); + if (cmTextarea === textarea) { + codeMirror = instance.codemirror; + break; + } + } + } + + // Alternative: try to get CodeMirror from the textarea's parent + if (!codeMirror && textarea.parentElement) { + const parent = textarea.parentElement; + if (parent._easyMDEInstance && parent._easyMDEInstance.codemirror) { + codeMirror = parent._easyMDEInstance.codemirror; + } + } + } + + const target = codeMirror || textarea; + + if (target) { + // Add paste handler + if (codeMirror) { + codeMirror.on('paste', function(cm, event) { + handleManualPaste(event, cm); + }); + } else if (textarea) { + textarea.addEventListener('paste', function(event) { + handleManualPaste(event, textarea); + }, true); // Use capture phase to intercept early + } + return; // Success - we're done + } + } + + // If we didn't find the textarea, retry + if (attempts < maxAttempts) { + setTimeout(tryInitialize, attempts * 100); + } + }; + + // Start with initial delay to allow EasyMDE to initialize + setTimeout(tryInitialize, 100); + } +}; + +/** + * Handler for converting pasted spreadsheet tables to Markdown tables. + * Handles both HTML tables (from Google Sheets) and tab-separated values. + */ +function handleManualPaste(event, target) { + const clipboardData = event.clipboardData || window.clipboardData; + if (!clipboardData) return; + + const html = clipboardData.getData('text/html'); + const text = clipboardData.getData('text/plain'); + + let markdownTable = null; + + // Check if HTML contains a table + if (html && html.includes('')) { + markdownTable = convertHtmlTableToMarkdown(html); + } + // Check if it's tab-separated text (spreadsheet cells) + else if (text && text.includes('\t') && text.includes('\n')) { + const rows = text.trim().split(/\r?\n/).filter(row => row.trim().length > 0); + if (rows.length < 1) return; + + const cells = rows.map(row => row.split('\t').map(cell => cell.trim())); + const maxCols = Math.max(...cells.map(row => row.length)); + + // Pad rows to have same number of columns + const paddedCells = cells.map(row => { + while (row.length < maxCols) row.push(''); + return row; + }); + + // Escape pipe characters in cells + const escapeCell = (cell) => cell.replace(/\|/g, '\\|'); + + // Build Markdown table + const header = paddedCells[0]; + const body = paddedCells.slice(1); + + const headerRow = '| ' + header.map(escapeCell).join(' | ') + ' |'; + const separatorRow = '| ' + header.map(() => '---').join(' | ') + ' |'; + const bodyRows = body.map(row => '| ' + row.map(escapeCell).join(' | ') + ' |'); + + markdownTable = [headerRow, separatorRow, ...bodyRows].join('\n') + '\n'; + } + + // If we have a table to insert, do it + if (markdownTable) { + event.preventDefault(); + event.stopPropagation(); + + // Insert into editor + if (target.getDoc && target.getDoc().replaceRange) { + // CodeMirror + const doc = target.getDoc(); + const cursor = doc.getCursor(); + doc.replaceRange(markdownTable, cursor); + } else if (target.setRangeText) { + // Textarea with setRangeText + const start = target.selectionStart; + const end = target.selectionEnd; + target.setRangeText(markdownTable, start, end, 'end'); + target.dispatchEvent(new Event('input', { bubbles: true })); + } else { + // Fallback: insert at cursor + const start = target.selectionStart || 0; + const end = target.selectionEnd || 0; + const before = target.value.substring(0, start); + const after = target.value.substring(end); + target.value = before + markdownTable + after; + target.selectionStart = target.selectionEnd = before.length + markdownTable.length; + target.dispatchEvent(new Event('input', { bubbles: true })); + } + } +} + +/** + * Converts an HTML table to Markdown table format + */ +function convertHtmlTableToMarkdown(html) { + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = html; + + const table = tempDiv.querySelector('table'); + if (!table) return null; + + const rows = table.querySelectorAll('tr'); + if (rows.length === 0) return null; + + const escapeCell = (cell) => { + const text = cell.textContent || cell.innerText || ''; + return text.trim().replace(/\|/g, '\\|').replace(/\n/g, ' '); + }; + + const markdownRows = []; + + // Process header row (first row) + const headerRow = rows[0]; + const headerCells = headerRow.querySelectorAll('th, td'); + const headerText = Array.from(headerCells).map(escapeCell); + markdownRows.push('| ' + headerText.join(' | ') + ' |'); + + // Add separator + markdownRows.push('| ' + headerText.map(() => '---').join(' | ') + ' |'); + + // Process data rows + for (let i = 1; i < rows.length; i++) { + const row = rows[i]; + const cells = row.querySelectorAll('td, th'); + const cellText = Array.from(cells).map(escapeCell); + markdownRows.push('| ' + cellText.join(' | ') + ' |'); + } + + return markdownRows.join('\n') + '\n'; +}