Files
chapter-organizer/WebApp/wwwroot/js/markdownTablePaste.js
T
poprhythm 84b31800ad 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.
2026-01-17 11:33:57 -05:00

230 lines
9.2 KiB
JavaScript

// 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';
}