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:
@@ -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