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.
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
|
||||
<script src="_content/PSC.Blazor.Components.MarkdownEditor/js/easymde.min.js"></script>
|
||||
<script src="_content/PSC.Blazor.Components.MarkdownEditor/js/markdownEditor.js"></script>
|
||||
<script src="js/markdownTablePaste.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
@inject INotesService NotesService
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IDialogService DialogService
|
||||
@inject MarkdownTablePasteService MarkdownTablePasteService
|
||||
|
||||
<MudDialog Class="@MarkdownHelper.GetNoteColorClass(_note.Id)">
|
||||
<DialogContent>
|
||||
@@ -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))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@namespace WebApp.Components.Shared.Components
|
||||
@inject INotesService NotesService
|
||||
@inject ISnackbar Snackbar
|
||||
@inject MarkdownTablePasteService MarkdownTablePasteService
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<MudDialog Class="@MarkdownHelper.GetNoteColorClass(_note.Id)">
|
||||
@@ -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()
|
||||
|
||||
@@ -194,6 +194,7 @@ builder.Services.AddScoped<Core.Services.IEventOccurrenceParserService>(sp =>
|
||||
builder.Services.AddScoped<WebApp.Services.FormValidationService>();
|
||||
builder.Services.AddScoped<WebApp.Services.EventDefinitionService>();
|
||||
builder.Services.AddScoped<WebApp.Services.INotesService, WebApp.Services.NotesService>();
|
||||
builder.Services.AddScoped<WebApp.Services.MarkdownTablePasteService>();
|
||||
|
||||
// State container for maintaining state per user connection (Blazor Server)
|
||||
builder.Services.AddScoped<StateContainer>();
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace WebApp.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for initializing paste-markdown functionality in MarkdownEditor components.
|
||||
/// Enables automatic conversion of pasted spreadsheet tables (Google Sheets, Excel) to Markdown tables.
|
||||
/// </summary>
|
||||
public class MarkdownTablePasteService
|
||||
{
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
private readonly ILogger<MarkdownTablePasteService> _logger;
|
||||
|
||||
public MarkdownTablePasteService(IJSRuntime jsRuntime, ILogger<MarkdownTablePasteService> logger)
|
||||
{
|
||||
_jsRuntime = jsRuntime;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes paste-markdown for a MarkdownEditor instance.
|
||||
/// Should be called after the editor has been rendered and EasyMDE has initialized.
|
||||
/// </summary>
|
||||
/// <param name="editorId">Optional ID of the editor wrapper element. If null, will attempt to find the most recent editor.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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('<table') && html.includes('</table>')) {
|
||||
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';
|
||||
}
|
||||
Reference in New Issue
Block a user