mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 18:10:18 +05:30
New Features: Enhanced Markdown Toolbar - Added strikethrough button - Added code block button - Added horizontal rule button Fixed Find & Replace - Fixed focus issue - Visual highlighting with text selection - Smart scroll to matches Fixed Line Numbers - Corrected line counting - Added scroll synchronization ConvertAPI Integration - Cloud conversion support (200+ formats) - Secure API key storage - Real-time conversion status
1645 lines
55 KiB
JavaScript
1645 lines
55 KiB
JavaScript
const { ipcRenderer } = require('electron');
|
||
const marked = require('marked');
|
||
const DOMPurify = require('dompurify');
|
||
const hljs = require('highlight.js');
|
||
|
||
// Configure marked
|
||
marked.setOptions({
|
||
highlight: function(code, lang) {
|
||
if (lang && hljs.getLanguage(lang)) {
|
||
try {
|
||
return hljs.highlight(code, { language: lang }).value;
|
||
} catch (err) {
|
||
// Fallback to auto highlighting if language-specific highlighting fails
|
||
console.warn('Syntax highlighting failed for language:', lang, err.message);
|
||
}
|
||
}
|
||
return hljs.highlightAuto(code).value;
|
||
},
|
||
breaks: true,
|
||
gfm: true
|
||
});
|
||
|
||
// Tab Management
|
||
class TabManager {
|
||
constructor() {
|
||
this.tabs = new Map();
|
||
this.activeTabId = 1;
|
||
this.nextTabId = 2;
|
||
this.isPreviewVisible = true;
|
||
this.showLineNumbers = false;
|
||
this.autoSaveInterval = null;
|
||
this.autoSaveDelay = 30000; // 30 seconds
|
||
this.recentFiles = JSON.parse(localStorage.getItem('recentFiles') || '[]');
|
||
|
||
// Initialize first tab
|
||
this.tabs.set(1, {
|
||
id: 1,
|
||
title: 'Untitled',
|
||
content: '',
|
||
filePath: null,
|
||
isDirty: false,
|
||
undoStack: [],
|
||
redoStack: [],
|
||
findMatches: [],
|
||
currentMatchIndex: -1
|
||
});
|
||
|
||
this.setupEventListeners();
|
||
this.updateUI();
|
||
}
|
||
|
||
setupEventListeners() {
|
||
// Tab bar events
|
||
document.getElementById('new-tab-btn').addEventListener('click', () => this.createNewTab());
|
||
document.getElementById('tab-bar').addEventListener('click', (e) => {
|
||
if (e.target.classList.contains('tab-close')) {
|
||
e.stopPropagation();
|
||
const tabId = parseInt(e.target.closest('.tab').dataset.tabId);
|
||
this.closeTab(tabId);
|
||
} else if (e.target.closest('.tab')) {
|
||
const tabId = parseInt(e.target.closest('.tab').dataset.tabId);
|
||
this.switchToTab(tabId);
|
||
}
|
||
});
|
||
|
||
// Editor events for active tab
|
||
this.setupEditorEvents();
|
||
|
||
// Toolbar events
|
||
this.setupToolbarEvents();
|
||
|
||
// Find dialog events
|
||
this.setupFindEvents();
|
||
|
||
// Keyboard shortcuts
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.ctrlKey || e.metaKey) {
|
||
switch (e.key) {
|
||
case 'n':
|
||
case 't':
|
||
e.preventDefault();
|
||
this.createNewTab();
|
||
break;
|
||
case 'w':
|
||
if (this.tabs.size > 1) {
|
||
e.preventDefault();
|
||
this.closeTab(this.activeTabId);
|
||
}
|
||
break;
|
||
case 'Tab':
|
||
if (this.tabs.size > 1) {
|
||
e.preventDefault();
|
||
this.switchToNextTab();
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
createNewTab() {
|
||
const newTabId = this.nextTabId++;
|
||
const tab = {
|
||
id: newTabId,
|
||
title: 'Untitled',
|
||
content: '',
|
||
filePath: null,
|
||
isDirty: false,
|
||
undoStack: [],
|
||
redoStack: [],
|
||
findMatches: [],
|
||
currentMatchIndex: -1
|
||
};
|
||
|
||
this.tabs.set(newTabId, tab);
|
||
this.createTabElements(tab);
|
||
this.switchToTab(newTabId);
|
||
this.startAutoSave();
|
||
this.updateTabBar();
|
||
}
|
||
|
||
createTabElements(tab) {
|
||
// Create tab content container
|
||
const tabContent = document.createElement('div');
|
||
tabContent.className = 'tab-content';
|
||
tabContent.id = `tab-content-${tab.id}`;
|
||
tabContent.dataset.tabId = tab.id;
|
||
|
||
tabContent.innerHTML = `
|
||
<div id="editor-pane-${tab.id}" class="pane">
|
||
<div class="editor-wrapper">
|
||
<div id="line-numbers-${tab.id}" class="line-numbers hidden"></div>
|
||
<textarea id="editor-${tab.id}" class="editor-textarea"></textarea>
|
||
</div>
|
||
</div>
|
||
<div id="preview-pane-${tab.id}" class="pane">
|
||
<div id="preview-${tab.id}" class="preview-content"></div>
|
||
</div>
|
||
`;
|
||
|
||
document.querySelector('.editor-container').appendChild(tabContent);
|
||
|
||
// Directly attach input listener to the new editor
|
||
const editor = document.getElementById(`editor-${tab.id}`);
|
||
if (editor) {
|
||
editor.addEventListener('input', () => {
|
||
this.handleEditorInput(tab.id);
|
||
});
|
||
|
||
// Add scroll listener for line number sync
|
||
editor.addEventListener('scroll', () => {
|
||
if (this.showLineNumbers && this.activeTabId === tab.id) {
|
||
const lineNumbers = document.getElementById(`line-numbers-${tab.id}`);
|
||
if (lineNumbers) {
|
||
lineNumbers.scrollTop = editor.scrollTop;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
switchToTab(tabId) {
|
||
if (!this.tabs.has(tabId)) return;
|
||
|
||
// Save current tab state before switching
|
||
if (this.activeTabId && this.tabs.has(this.activeTabId)) {
|
||
this.saveCurrentTabState();
|
||
}
|
||
|
||
this.activeTabId = tabId;
|
||
this.updateUI();
|
||
this.restoreTabState(tabId);
|
||
this.focusActiveEditor();
|
||
|
||
// Notify main process about current file for exports
|
||
const tab = this.tabs.get(tabId);
|
||
if (tab?.filePath) {
|
||
ipcRenderer.send('set-current-file', tab.filePath);
|
||
}
|
||
}
|
||
|
||
switchToNextTab() {
|
||
const tabIds = Array.from(this.tabs.keys());
|
||
const currentIndex = tabIds.indexOf(this.activeTabId);
|
||
const nextIndex = (currentIndex + 1) % tabIds.length;
|
||
this.switchToTab(tabIds[nextIndex]);
|
||
}
|
||
|
||
closeTab(tabId) {
|
||
if (this.tabs.size === 1) return; // Don't close the last tab
|
||
|
||
const tab = this.tabs.get(tabId);
|
||
if (tab.isDirty) {
|
||
// Show confirmation dialog for unsaved changes
|
||
const result = confirm('You have unsaved changes. Do you want to close this tab without saving?');
|
||
if (!result) return;
|
||
}
|
||
|
||
// Remove tab elements
|
||
const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`);
|
||
const tabContent = document.getElementById(`tab-content-${tabId}`);
|
||
|
||
if (tabElement?.classList.contains('tab')) {
|
||
tabElement.remove();
|
||
}
|
||
if (tabContent) {
|
||
tabContent.remove();
|
||
}
|
||
|
||
this.tabs.delete(tabId);
|
||
|
||
// Switch to another tab if this was active
|
||
if (this.activeTabId === tabId) {
|
||
const remainingTabs = Array.from(this.tabs.keys());
|
||
this.switchToTab(remainingTabs[0]);
|
||
}
|
||
|
||
this.updateTabBar();
|
||
}
|
||
|
||
updateTabBar() {
|
||
const tabBar = document.getElementById('tab-bar');
|
||
const existingTabs = tabBar.querySelectorAll('.tab');
|
||
|
||
// Remove all existing tab elements except the new tab button
|
||
existingTabs.forEach(tab => tab.remove());
|
||
|
||
// Add tabs in order
|
||
const sortedTabs = Array.from(this.tabs.values()).sort((a, b) => a.id - b.id);
|
||
const newTabBtn = document.getElementById('new-tab-btn');
|
||
|
||
sortedTabs.forEach(tab => {
|
||
const tabElement = document.createElement('div');
|
||
tabElement.className = `tab ${tab.id === this.activeTabId ? 'active' : ''}`;
|
||
tabElement.dataset.tabId = tab.id;
|
||
|
||
const title = tab.filePath ?
|
||
tab.filePath.split('/').pop() :
|
||
tab.title;
|
||
|
||
const dirtyIndicator = tab.isDirty ? ' •' : '';
|
||
|
||
tabElement.innerHTML = `
|
||
<span class="tab-title">${title}${dirtyIndicator}</span>
|
||
<button class="tab-close" title="Close tab">×</button>
|
||
`;
|
||
|
||
tabBar.insertBefore(tabElement, newTabBtn);
|
||
});
|
||
}
|
||
|
||
updateUI() {
|
||
// Show/hide tab contents
|
||
document.querySelectorAll('.tab-content').forEach(content => {
|
||
content.classList.remove('active');
|
||
});
|
||
|
||
const activeContent = document.getElementById(`tab-content-${this.activeTabId}`);
|
||
if (activeContent) {
|
||
activeContent.classList.add('active');
|
||
}
|
||
|
||
// Update preview visibility
|
||
this.updatePreviewVisibility();
|
||
this.updateLineNumbers();
|
||
this.updateTabBar();
|
||
}
|
||
|
||
saveCurrentTabState() {
|
||
const tab = this.tabs.get(this.activeTabId);
|
||
if (!tab) return;
|
||
|
||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
||
if (editor) {
|
||
tab.content = editor.value;
|
||
tab.isDirty = tab.content !== (tab.originalContent || '');
|
||
}
|
||
}
|
||
|
||
restoreTabState(tabId) {
|
||
const tab = this.tabs.get(tabId);
|
||
if (!tab) return;
|
||
|
||
const editor = document.getElementById(`editor-${tabId}`);
|
||
|
||
if (editor) {
|
||
editor.value = tab.content;
|
||
this.updatePreview(tabId);
|
||
this.updateWordCount();
|
||
}
|
||
}
|
||
|
||
focusActiveEditor() {
|
||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
||
if (editor) {
|
||
editor.focus();
|
||
}
|
||
}
|
||
|
||
updatePreview(tabId = this.activeTabId) {
|
||
const tab = this.tabs.get(tabId);
|
||
const preview = document.getElementById(`preview-${tabId}`);
|
||
|
||
if (!tab || !preview) return;
|
||
|
||
try {
|
||
const html = marked.parse(tab.content);
|
||
const sanitizedHtml = DOMPurify.sanitize(html);
|
||
preview.innerHTML = sanitizedHtml;
|
||
|
||
// Render math expressions if KaTeX is available
|
||
if (window.katex && window.renderMathInElement) {
|
||
try {
|
||
window.renderMathInElement(preview, {
|
||
delimiters: [
|
||
{left: '$$', right: '$$', display: true},
|
||
{left: '$', right: '$', display: false},
|
||
{left: '\\[', right: '\\]', display: true},
|
||
{left: '\\(', right: '\\)', display: false}
|
||
]
|
||
});
|
||
} catch (mathError) {
|
||
console.warn('Math rendering error:', mathError);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error rendering preview:', error);
|
||
preview.innerHTML = '<p class="error">Error rendering preview. Please check your markdown syntax.</p>';
|
||
}
|
||
}
|
||
|
||
updatePreviewVisibility() {
|
||
document.querySelectorAll('.tab-content').forEach(content => {
|
||
const previewPane = content.querySelector('.pane:last-child');
|
||
const editorPane = content.querySelector('.pane:first-child');
|
||
|
||
if (this.isPreviewVisible) {
|
||
previewPane.classList.remove('hidden');
|
||
editorPane.classList.remove('full-width');
|
||
} else {
|
||
previewPane.classList.add('hidden');
|
||
editorPane.classList.add('full-width');
|
||
}
|
||
});
|
||
}
|
||
|
||
updateLineNumbers() {
|
||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
||
const lineNumbers = document.getElementById(`line-numbers-${this.activeTabId}`);
|
||
|
||
if (!editor || !lineNumbers) return;
|
||
|
||
if (this.showLineNumbers) {
|
||
const lines = editor.value.split('\n');
|
||
lineNumbers.innerHTML = lines.map((_, i) =>
|
||
`<div class="line-number">${i + 1}</div>`
|
||
).join('');
|
||
lineNumbers.classList.remove('hidden');
|
||
|
||
// Sync scroll position
|
||
lineNumbers.scrollTop = editor.scrollTop;
|
||
} else {
|
||
lineNumbers.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
updateWordCount() {
|
||
const tab = this.tabs.get(this.activeTabId);
|
||
if (!tab) return;
|
||
|
||
const content = tab.content;
|
||
const words = content.trim() ? content.trim().split(/\s+/).filter(word => word.length > 0).length : 0;
|
||
const chars = content.length;
|
||
const charsNoSpaces = content.replace(/\s/g, '').length;
|
||
|
||
// Enhanced statistics
|
||
const lines = content.split('\n').length;
|
||
const paragraphs = content.split(/\n\s*\n/).filter(p => p.trim()).length;
|
||
const readingTime = Math.ceil(words / 200); // Average reading speed: 200 words/minute
|
||
const sentences = content.split(/[.!?]+/).filter(s => s.trim()).length;
|
||
|
||
// Update the word count display with enhanced stats
|
||
const basicStats = `Words: ${words} | Characters: ${chars} (${charsNoSpaces} no spaces)`;
|
||
const enhancedStats = `Lines: ${lines} | Paragraphs: ${paragraphs} | Sentences: ${sentences} | Reading time: ${readingTime} min`;
|
||
|
||
document.getElementById('word-count').textContent = basicStats;
|
||
|
||
// Add enhanced stats to a separate element
|
||
let enhancedEl = document.getElementById('enhanced-stats');
|
||
if (!enhancedEl) {
|
||
enhancedEl = document.createElement('div');
|
||
enhancedEl.id = 'enhanced-stats';
|
||
enhancedEl.className = 'enhanced-stats';
|
||
document.querySelector('.status-bar').appendChild(enhancedEl);
|
||
}
|
||
enhancedEl.textContent = enhancedStats;
|
||
}
|
||
|
||
setupEditorEvents() {
|
||
// Set up editor events using event delegation on the container
|
||
const editorContainer = document.querySelector('.editor-container');
|
||
if (editorContainer) {
|
||
editorContainer.addEventListener('input', (e) => {
|
||
if (e.target.classList.contains('editor-textarea')) {
|
||
const tabId = parseInt(e.target.id.split('-')[1]);
|
||
this.handleEditorInput(tabId);
|
||
}
|
||
});
|
||
|
||
editorContainer.addEventListener('scroll', (e) => {
|
||
if (e.target.classList.contains('editor-textarea')) {
|
||
this.updateLineNumbers();
|
||
}
|
||
}, true);
|
||
}
|
||
}
|
||
|
||
handleEditorInput(tabId) {
|
||
const tab = this.tabs.get(tabId);
|
||
if (!tab) return;
|
||
|
||
const editor = document.getElementById(`editor-${tabId}`);
|
||
if (!editor) return;
|
||
|
||
tab.content = editor.value;
|
||
tab.isDirty = true;
|
||
|
||
this.updatePreview(tabId);
|
||
this.updateWordCount();
|
||
this.updateLineNumbers();
|
||
this.updateTabBar();
|
||
|
||
// Add to undo stack
|
||
this.pushUndoState(tabId);
|
||
}
|
||
|
||
pushUndoState(tabId) {
|
||
const tab = this.tabs.get(tabId);
|
||
if (!tab) return;
|
||
|
||
tab.undoStack.push(tab.content);
|
||
if (tab.undoStack.length > 50) {
|
||
tab.undoStack.shift();
|
||
}
|
||
tab.redoStack = [];
|
||
}
|
||
|
||
undo() {
|
||
const tab = this.tabs.get(this.activeTabId);
|
||
if (!tab || tab.undoStack.length === 0) return;
|
||
|
||
tab.redoStack.push(tab.content);
|
||
tab.content = tab.undoStack.pop();
|
||
|
||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
||
if (editor) {
|
||
editor.value = tab.content;
|
||
this.updatePreview();
|
||
this.updateWordCount();
|
||
}
|
||
}
|
||
|
||
redo() {
|
||
const tab = this.tabs.get(this.activeTabId);
|
||
if (!tab || tab.redoStack.length === 0) return;
|
||
|
||
tab.undoStack.push(tab.content);
|
||
tab.content = tab.redoStack.pop();
|
||
|
||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
||
if (editor) {
|
||
editor.value = tab.content;
|
||
this.updatePreview();
|
||
this.updateWordCount();
|
||
}
|
||
}
|
||
|
||
// Auto-save functionality
|
||
startAutoSave() {
|
||
if (this.autoSaveInterval) {
|
||
clearInterval(this.autoSaveInterval);
|
||
}
|
||
|
||
this.autoSaveInterval = setInterval(() => {
|
||
this.performAutoSave();
|
||
}, this.autoSaveDelay);
|
||
}
|
||
|
||
stopAutoSave() {
|
||
if (this.autoSaveInterval) {
|
||
clearInterval(this.autoSaveInterval);
|
||
this.autoSaveInterval = null;
|
||
}
|
||
}
|
||
|
||
performAutoSave() {
|
||
const tab = this.tabs.get(this.activeTabId);
|
||
if (!tab || !tab.filePath || !tab.content) return;
|
||
|
||
// Only auto-save if content has changed since last save
|
||
if (tab.lastSavedContent !== tab.content) {
|
||
ipcRenderer.send('save-file', { path: tab.filePath, content: tab.content });
|
||
tab.lastSavedContent = tab.content;
|
||
|
||
// Show brief auto-save indicator
|
||
this.showAutoSaveIndicator();
|
||
}
|
||
}
|
||
|
||
showAutoSaveIndicator() {
|
||
const indicator = document.createElement('div');
|
||
indicator.textContent = 'Auto-saved';
|
||
indicator.className = 'auto-save-indicator';
|
||
document.body.appendChild(indicator);
|
||
|
||
setTimeout(() => {
|
||
indicator.classList.add('fade-out');
|
||
setTimeout(() => {
|
||
if (indicator.parentNode) {
|
||
indicator.parentNode.removeChild(indicator);
|
||
}
|
||
}, 300);
|
||
}, 1500);
|
||
}
|
||
|
||
// Recent files functionality
|
||
addToRecentFiles(filePath) {
|
||
if (!filePath) return;
|
||
|
||
// Remove if already exists
|
||
this.recentFiles = this.recentFiles.filter(f => f !== filePath);
|
||
|
||
// Add to beginning
|
||
this.recentFiles.unshift(filePath);
|
||
|
||
// Keep only last 10 files
|
||
this.recentFiles = this.recentFiles.slice(0, 10);
|
||
|
||
// Save to localStorage and sync with main process
|
||
localStorage.setItem('recentFiles', JSON.stringify(this.recentFiles));
|
||
ipcRenderer.send('save-recent-files', this.recentFiles);
|
||
}
|
||
|
||
getRecentFiles() {
|
||
return this.recentFiles.filter(file => {
|
||
// Check if file still exists (basic check by trying to access it)
|
||
try {
|
||
return file && file.length > 0;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
});
|
||
}
|
||
|
||
setupToolbarEvents() {
|
||
// Bold
|
||
document.getElementById('btn-bold').addEventListener('click', () => {
|
||
this.wrapSelection('**', '**');
|
||
});
|
||
|
||
// Italic
|
||
document.getElementById('btn-italic').addEventListener('click', () => {
|
||
this.wrapSelection('*', '*');
|
||
});
|
||
|
||
// Heading
|
||
document.getElementById('btn-heading').addEventListener('click', () => {
|
||
this.insertAtLineStart('## ');
|
||
});
|
||
|
||
// Link
|
||
document.getElementById('btn-link').addEventListener('click', () => {
|
||
this.wrapSelection('[', '](url)');
|
||
});
|
||
|
||
// Code
|
||
document.getElementById('btn-code').addEventListener('click', () => {
|
||
this.wrapSelection('`', '`');
|
||
});
|
||
|
||
// List
|
||
document.getElementById('btn-list').addEventListener('click', () => {
|
||
this.insertAtLineStart('- ');
|
||
});
|
||
|
||
// Quote
|
||
document.getElementById('btn-quote').addEventListener('click', () => {
|
||
this.insertAtLineStart('> ');
|
||
});
|
||
|
||
// Table
|
||
document.getElementById('btn-table').addEventListener('click', () => {
|
||
this.insertTable();
|
||
});
|
||
|
||
// Strikethrough
|
||
document.getElementById('btn-strikethrough').addEventListener('click', () => {
|
||
this.wrapSelection('~~', '~~');
|
||
});
|
||
|
||
// Code Block
|
||
document.getElementById('btn-code-block').addEventListener('click', () => {
|
||
this.insertCodeBlock();
|
||
});
|
||
|
||
// Horizontal Rule
|
||
document.getElementById('btn-horizontal-rule').addEventListener('click', () => {
|
||
this.insertHorizontalRule();
|
||
});
|
||
|
||
// Preview toggle
|
||
document.getElementById('btn-preview-toggle').addEventListener('click', () => {
|
||
this.isPreviewVisible = !this.isPreviewVisible;
|
||
this.updatePreviewVisibility();
|
||
});
|
||
|
||
// Line numbers
|
||
document.getElementById('btn-line-numbers').addEventListener('click', () => {
|
||
this.showLineNumbers = !this.showLineNumbers;
|
||
this.updateLineNumbers();
|
||
});
|
||
}
|
||
|
||
// Helper function to wrap selected text
|
||
wrapSelection(before, after) {
|
||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
||
if (!editor) return;
|
||
|
||
const start = editor.selectionStart;
|
||
const end = editor.selectionEnd;
|
||
const selectedText = editor.value.substring(start, end);
|
||
const replacement = before + (selectedText || 'text') + after;
|
||
|
||
editor.value = editor.value.substring(0, start) + replacement + editor.value.substring(end);
|
||
|
||
// Update cursor position
|
||
const newCursorPos = selectedText ? start + replacement.length : start + before.length;
|
||
editor.selectionStart = editor.selectionEnd = newCursorPos;
|
||
editor.focus();
|
||
|
||
// Trigger update
|
||
this.handleEditorInput(this.activeTabId);
|
||
}
|
||
|
||
// Helper function to insert text at the start of current line
|
||
insertAtLineStart(prefix) {
|
||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
||
if (!editor) return;
|
||
|
||
const start = editor.selectionStart;
|
||
const text = editor.value;
|
||
|
||
// Find the start of the current line
|
||
let lineStart = text.lastIndexOf('\n', start - 1) + 1;
|
||
|
||
// Insert the prefix
|
||
editor.value = text.substring(0, lineStart) + prefix + text.substring(lineStart);
|
||
|
||
// Update cursor position
|
||
editor.selectionStart = editor.selectionEnd = start + prefix.length;
|
||
editor.focus();
|
||
|
||
// Trigger update
|
||
this.handleEditorInput(this.activeTabId);
|
||
}
|
||
|
||
// Insert a markdown table
|
||
insertTable() {
|
||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
||
if (!editor) return;
|
||
|
||
const table = '\n| Column 1 | Column 2 | Column 3 |\n' +
|
||
'|----------|----------|----------|\n' +
|
||
'| Cell 1 | Cell 2 | Cell 3 |\n' +
|
||
'| Cell 4 | Cell 5 | Cell 6 |\n';
|
||
|
||
const start = editor.selectionStart;
|
||
editor.value = editor.value.substring(0, start) + table + editor.value.substring(start);
|
||
|
||
// Update cursor position
|
||
editor.selectionStart = editor.selectionEnd = start + table.length;
|
||
editor.focus();
|
||
|
||
// Trigger update
|
||
this.handleEditorInput(this.activeTabId);
|
||
}
|
||
|
||
// Insert a code block
|
||
insertCodeBlock() {
|
||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
||
if (!editor) return;
|
||
|
||
const selectedText = editor.value.substring(editor.selectionStart, editor.selectionEnd);
|
||
const codeBlock = '\n```\n' + (selectedText || 'code here') + '\n```\n';
|
||
|
||
const start = editor.selectionStart;
|
||
editor.value = editor.value.substring(0, start) + codeBlock + editor.value.substring(editor.selectionEnd);
|
||
|
||
// Update cursor position
|
||
editor.selectionStart = editor.selectionEnd = start + codeBlock.length;
|
||
editor.focus();
|
||
|
||
// Trigger update
|
||
this.handleEditorInput(this.activeTabId);
|
||
}
|
||
|
||
// Insert a horizontal rule
|
||
insertHorizontalRule() {
|
||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
||
if (!editor) return;
|
||
|
||
const hr = '\n\n---\n\n';
|
||
const start = editor.selectionStart;
|
||
|
||
editor.value = editor.value.substring(0, start) + hr + editor.value.substring(start);
|
||
|
||
// Update cursor position
|
||
editor.selectionStart = editor.selectionEnd = start + hr.length;
|
||
editor.focus();
|
||
|
||
// Trigger update
|
||
this.handleEditorInput(this.activeTabId);
|
||
}
|
||
|
||
setupFindEvents() {
|
||
const btnFind = document.getElementById('btn-find');
|
||
const btnFindClose = document.getElementById('btn-find-close');
|
||
const findInput = document.getElementById('find-input');
|
||
const btnFindNext = document.getElementById('btn-find-next');
|
||
const btnFindPrev = document.getElementById('btn-find-prev');
|
||
const btnReplace = document.getElementById('btn-replace');
|
||
const btnReplaceAll = document.getElementById('btn-replace-all');
|
||
|
||
if (!btnFind || !btnFindClose || !findInput || !btnFindNext || !btnFindPrev || !btnReplace || !btnReplaceAll) {
|
||
console.error('Find dialog elements not found');
|
||
return;
|
||
}
|
||
|
||
// Show find dialog
|
||
btnFind.addEventListener('click', () => {
|
||
document.getElementById('find-dialog').classList.remove('hidden');
|
||
findInput.focus();
|
||
});
|
||
|
||
// Close find dialog
|
||
btnFindClose.addEventListener('click', () => {
|
||
document.getElementById('find-dialog').classList.add('hidden');
|
||
this.clearFindHighlights();
|
||
});
|
||
|
||
// Find input change - update matches
|
||
findInput.addEventListener('input', () => {
|
||
this.performFind();
|
||
});
|
||
|
||
// Find next
|
||
btnFindNext.addEventListener('click', () => {
|
||
this.findNext();
|
||
});
|
||
|
||
// Find previous
|
||
btnFindPrev.addEventListener('click', () => {
|
||
this.findPrevious();
|
||
});
|
||
|
||
// Replace
|
||
btnReplace.addEventListener('click', () => {
|
||
this.replaceOne();
|
||
});
|
||
|
||
// Replace all
|
||
btnReplaceAll.addEventListener('click', () => {
|
||
this.replaceAll();
|
||
});
|
||
|
||
// Enter key in find input - find next
|
||
findInput.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
if (e.shiftKey) {
|
||
this.findPrevious();
|
||
} else {
|
||
this.findNext();
|
||
}
|
||
}
|
||
});
|
||
|
||
}
|
||
|
||
performFind() {
|
||
const findText = document.getElementById('find-input').value;
|
||
const tab = this.tabs.get(this.activeTabId);
|
||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
||
|
||
if (!findText || !tab || !editor) {
|
||
this.clearFindHighlights();
|
||
const findCount = document.getElementById('find-count');
|
||
if (findCount) {
|
||
findCount.textContent = '0 matches';
|
||
}
|
||
return;
|
||
}
|
||
|
||
const content = editor.value;
|
||
const matches = [];
|
||
let index = 0;
|
||
|
||
// Find all matches
|
||
while ((index = content.indexOf(findText, index)) !== -1) {
|
||
matches.push(index);
|
||
index += findText.length;
|
||
}
|
||
|
||
tab.findMatches = matches;
|
||
tab.currentMatchIndex = matches.length > 0 ? 0 : -1;
|
||
|
||
// Update match count
|
||
const findCount = document.getElementById('find-count');
|
||
if (findCount) {
|
||
findCount.textContent = `${matches.length} match${matches.length !== 1 ? 'es' : ''}`;
|
||
}
|
||
|
||
// Highlight first match
|
||
if (matches.length > 0) {
|
||
this.highlightMatch(0);
|
||
}
|
||
}
|
||
|
||
findNext() {
|
||
const tab = this.tabs.get(this.activeTabId);
|
||
if (!tab || tab.findMatches.length === 0) return;
|
||
|
||
tab.currentMatchIndex = (tab.currentMatchIndex + 1) % tab.findMatches.length;
|
||
this.highlightMatch(tab.currentMatchIndex);
|
||
}
|
||
|
||
findPrevious() {
|
||
const tab = this.tabs.get(this.activeTabId);
|
||
if (!tab || tab.findMatches.length === 0) return;
|
||
|
||
tab.currentMatchIndex = tab.currentMatchIndex - 1;
|
||
if (tab.currentMatchIndex < 0) {
|
||
tab.currentMatchIndex = tab.findMatches.length - 1;
|
||
}
|
||
this.highlightMatch(tab.currentMatchIndex);
|
||
}
|
||
|
||
highlightMatch(matchIndex) {
|
||
const tab = this.tabs.get(this.activeTabId);
|
||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
||
const findText = document.getElementById('find-input').value;
|
||
|
||
if (!tab || !editor || matchIndex < 0 || matchIndex >= tab.findMatches.length) return;
|
||
|
||
const position = tab.findMatches[matchIndex];
|
||
|
||
// Select the match WITHOUT focusing (to keep focus on find input)
|
||
editor.setSelectionRange(position, position + findText.length);
|
||
|
||
// Make the selection visible by briefly focusing and then restoring focus
|
||
const findInput = document.getElementById('find-input');
|
||
const hadFocus = document.activeElement === findInput;
|
||
|
||
// Temporarily focus editor to make selection visible
|
||
editor.focus();
|
||
|
||
// Restore focus to find input if it had focus
|
||
if (hadFocus) {
|
||
setTimeout(() => {
|
||
findInput.focus();
|
||
// Restore cursor position in find input
|
||
findInput.setSelectionRange(findInput.value.length, findInput.value.length);
|
||
}, 10);
|
||
}
|
||
|
||
// Scroll into view
|
||
const lineHeight = 20; // Approximate line height
|
||
const charPosition = position;
|
||
const numLines = editor.value.substring(0, charPosition).split('\n').length;
|
||
const scrollPosition = (numLines - 5) * lineHeight; // Show match 5 lines from top
|
||
|
||
editor.scrollTop = Math.max(0, scrollPosition);
|
||
|
||
// Update match counter
|
||
const findCount = document.getElementById('find-count');
|
||
if (findCount) {
|
||
findCount.textContent = `Match ${matchIndex + 1} of ${tab.findMatches.length}`;
|
||
}
|
||
}
|
||
|
||
replaceOne() {
|
||
const tab = this.tabs.get(this.activeTabId);
|
||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
||
const findText = document.getElementById('find-input').value;
|
||
const replaceText = document.getElementById('replace-input').value;
|
||
|
||
if (!tab || !editor || tab.findMatches.length === 0 || tab.currentMatchIndex < 0) return;
|
||
|
||
const position = tab.findMatches[tab.currentMatchIndex];
|
||
const before = editor.value.substring(0, position);
|
||
const after = editor.value.substring(position + findText.length);
|
||
|
||
editor.value = before + replaceText + after;
|
||
tab.content = editor.value;
|
||
tab.isDirty = true;
|
||
|
||
this.updatePreview(this.activeTabId);
|
||
this.updateWordCount();
|
||
this.updateTabBar();
|
||
|
||
// Re-perform find to update matches
|
||
this.performFind();
|
||
}
|
||
|
||
replaceAll() {
|
||
const tab = this.tabs.get(this.activeTabId);
|
||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
||
const findText = document.getElementById('find-input').value;
|
||
const replaceText = document.getElementById('replace-input').value;
|
||
|
||
if (!tab || !editor || !findText) return;
|
||
|
||
// Simple replace all
|
||
const newContent = editor.value.split(findText).join(replaceText);
|
||
const replacedCount = tab.findMatches.length;
|
||
|
||
editor.value = newContent;
|
||
tab.content = newContent;
|
||
tab.isDirty = true;
|
||
|
||
this.updatePreview(this.activeTabId);
|
||
this.updateWordCount();
|
||
this.updateTabBar();
|
||
|
||
// Update match count
|
||
document.getElementById('find-count').textContent = `Replaced ${replacedCount} match${replacedCount !== 1 ? 'es' : ''}`;
|
||
|
||
// Re-perform find
|
||
this.performFind();
|
||
}
|
||
|
||
clearFindHighlights() {
|
||
const tab = this.tabs.get(this.activeTabId);
|
||
if (tab) {
|
||
tab.findMatches = [];
|
||
tab.currentMatchIndex = -1;
|
||
}
|
||
}
|
||
|
||
// File operations
|
||
openFile(filePath, content) {
|
||
let tab = this.tabs.get(this.activeTabId);
|
||
|
||
// Handle both forward and back slashes for cross-platform compatibility
|
||
const fileName = filePath.split(/[\\/]/).pop();
|
||
|
||
// If current tab is empty and untitled, reuse it
|
||
if (!tab.filePath && !tab.isDirty && tab.content === '') {
|
||
tab.filePath = filePath;
|
||
tab.title = fileName;
|
||
tab.content = content;
|
||
tab.originalContent = content;
|
||
tab.isDirty = false;
|
||
} else {
|
||
// Create new tab for the file
|
||
this.createNewTab();
|
||
tab = this.tabs.get(this.activeTabId);
|
||
tab.filePath = filePath;
|
||
tab.title = fileName;
|
||
tab.content = content;
|
||
tab.originalContent = content;
|
||
tab.isDirty = false;
|
||
}
|
||
|
||
this.restoreTabState(this.activeTabId);
|
||
this.startAutoSave();
|
||
this.addToRecentFiles(filePath);
|
||
this.updateTabBar();
|
||
}
|
||
|
||
getCurrentContent() {
|
||
const tab = this.tabs.get(this.activeTabId);
|
||
return tab ? tab.content : '';
|
||
}
|
||
|
||
getCurrentFilePath() {
|
||
const tab = this.tabs.get(this.activeTabId);
|
||
return tab ? tab.filePath : null;
|
||
}
|
||
}
|
||
|
||
// Initialize tab manager
|
||
let tabManager;
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
tabManager = new TabManager();
|
||
|
||
// Attach input listener to the initial editor (tab 1)
|
||
const initialEditor = document.getElementById('editor-1');
|
||
if (initialEditor) {
|
||
initialEditor.addEventListener('input', () => {
|
||
tabManager.handleEditorInput(1);
|
||
});
|
||
|
||
// Add scroll listener for line number sync
|
||
initialEditor.addEventListener('scroll', () => {
|
||
if (tabManager.showLineNumbers && tabManager.activeTabId === 1) {
|
||
const lineNumbers = document.getElementById('line-numbers-1');
|
||
if (lineNumbers) {
|
||
lineNumbers.scrollTop = initialEditor.scrollTop;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Request current theme
|
||
ipcRenderer.send('get-theme');
|
||
|
||
// Signal that renderer is ready for file operations
|
||
ipcRenderer.send('renderer-ready');
|
||
|
||
// Set up auto-save interval
|
||
setInterval(() => {
|
||
// Auto-save logic for all tabs
|
||
tabManager.tabs.forEach(tab => {
|
||
if (tab.isDirty && tab.filePath) {
|
||
ipcRenderer.send('save-current-file', tab.content);
|
||
}
|
||
});
|
||
}, 30000);
|
||
});
|
||
|
||
// IPC event listeners
|
||
ipcRenderer.on('file-new', () => {
|
||
tabManager.createNewTab();
|
||
});
|
||
|
||
ipcRenderer.on('file-opened', (event, data) => {
|
||
if (tabManager) {
|
||
tabManager.openFile(data.path, data.content);
|
||
}
|
||
});
|
||
|
||
ipcRenderer.on('file-save', () => {
|
||
const currentContent = tabManager.getCurrentContent();
|
||
const currentFilePath = tabManager.getCurrentFilePath();
|
||
if (currentFilePath) {
|
||
ipcRenderer.send('save-current-file', currentContent);
|
||
}
|
||
});
|
||
|
||
ipcRenderer.on('get-content-for-save', (event, filePath) => {
|
||
const currentContent = tabManager.getCurrentContent();
|
||
ipcRenderer.send('save-file', { path: filePath, content: currentContent });
|
||
});
|
||
|
||
ipcRenderer.on('get-content-for-spreadsheet', (event, format) => {
|
||
const currentContent = tabManager.getCurrentContent();
|
||
ipcRenderer.send('export-spreadsheet', { content: currentContent, format });
|
||
});
|
||
|
||
ipcRenderer.on('toggle-preview', () => {
|
||
tabManager.isPreviewVisible = !tabManager.isPreviewVisible;
|
||
tabManager.updatePreviewVisibility();
|
||
});
|
||
|
||
ipcRenderer.on('toggle-find', () => {
|
||
const findDialog = document.getElementById('find-dialog');
|
||
if (findDialog.classList.contains('hidden')) {
|
||
findDialog.classList.remove('hidden');
|
||
document.getElementById('find-input').focus();
|
||
} else {
|
||
findDialog.classList.add('hidden');
|
||
}
|
||
});
|
||
|
||
ipcRenderer.on('theme-changed', (event, theme) => {
|
||
document.body.className = `theme-${theme}`;
|
||
});
|
||
|
||
// Font size adjustment
|
||
let currentFontSize = parseInt(localStorage.getItem('fontSize')) || 15;
|
||
|
||
function updateFontSizes(size) {
|
||
const editors = document.querySelectorAll('#editor, .editor-textarea');
|
||
const previews = document.querySelectorAll('#preview, .preview-content');
|
||
|
||
editors.forEach(editor => {
|
||
editor.style.fontSize = `${size}px`;
|
||
});
|
||
|
||
previews.forEach(preview => {
|
||
preview.style.fontSize = `${size}px`;
|
||
});
|
||
|
||
localStorage.setItem('fontSize', size);
|
||
}
|
||
|
||
// Apply saved font size on load
|
||
updateFontSizes(currentFontSize);
|
||
|
||
ipcRenderer.on('adjust-font-size', (event, action) => {
|
||
if (action === 'increase' && currentFontSize < 24) {
|
||
currentFontSize++;
|
||
} else if (action === 'decrease' && currentFontSize > 10) {
|
||
currentFontSize--;
|
||
} else if (action === 'reset') {
|
||
currentFontSize = 15;
|
||
}
|
||
updateFontSizes(currentFontSize);
|
||
});
|
||
|
||
// Export Dialog functionality
|
||
let currentExportFormat = null;
|
||
|
||
ipcRenderer.on('show-export-dialog', (event, format) => {
|
||
currentExportFormat = format;
|
||
showExportDialog(format);
|
||
});
|
||
|
||
function showExportDialog(format) {
|
||
const dialog = document.getElementById('export-dialog');
|
||
const title = document.getElementById('export-dialog-title');
|
||
|
||
title.textContent = `Export as ${format.toUpperCase()}`;
|
||
dialog.setAttribute('data-format', format);
|
||
dialog.classList.remove('hidden');
|
||
|
||
// Initialize form values
|
||
initializeExportForm(format);
|
||
}
|
||
|
||
function hideExportDialog() {
|
||
const dialog = document.getElementById('export-dialog');
|
||
dialog.classList.add('hidden');
|
||
currentExportFormat = null;
|
||
}
|
||
|
||
function initializeExportForm(format) {
|
||
// Reset advanced export toggle to unchecked
|
||
const advancedToggle = document.getElementById('advanced-export-toggle');
|
||
const advancedOptions = document.getElementById('advanced-export-options');
|
||
|
||
advancedToggle.checked = false;
|
||
advancedOptions.classList.add('hidden');
|
||
|
||
// Reset form to defaults
|
||
document.getElementById('export-template').value = 'default';
|
||
document.getElementById('custom-template-path').style.display = 'none';
|
||
|
||
// Clear metadata fields
|
||
const metadataFields = document.querySelectorAll('.metadata-field');
|
||
metadataFields.forEach((field, index) => {
|
||
if (index < 4) { // Keep first 4 default fields
|
||
field.querySelector('.metadata-key').value = ['title', 'author', 'date', 'subject'][index] || '';
|
||
field.querySelector('.metadata-value').value = '';
|
||
} else {
|
||
field.remove(); // Remove additional fields
|
||
}
|
||
});
|
||
|
||
// Reset checkboxes and other fields
|
||
document.getElementById('export-toc').checked = false;
|
||
document.getElementById('export-number-sections').checked = false;
|
||
document.getElementById('export-citeproc').checked = false;
|
||
document.getElementById('export-toc-depth').value = 3;
|
||
|
||
// PDF-specific fields
|
||
if (format === 'pdf') {
|
||
document.getElementById('pdf-engine').value = 'xelatex';
|
||
document.getElementById('pdf-geometry').value = 'margin=1in';
|
||
document.getElementById('custom-geometry').style.display = 'none';
|
||
}
|
||
|
||
// Clear bibliography fields
|
||
document.getElementById('bibliography-file').value = '';
|
||
document.getElementById('csl-file').value = '';
|
||
}
|
||
|
||
function collectExportOptions() {
|
||
const advancedMode = document.getElementById('advanced-export-toggle').checked;
|
||
const options = {};
|
||
|
||
if (advancedMode) {
|
||
// Collect advanced options
|
||
options.template = document.getElementById('export-template').value;
|
||
options.metadata = {};
|
||
options.variables = {};
|
||
options.toc = document.getElementById('export-toc').checked;
|
||
options.tocDepth = document.getElementById('export-toc-depth').value;
|
||
options.numberSections = document.getElementById('export-number-sections').checked;
|
||
options.citeproc = document.getElementById('export-citeproc').checked;
|
||
} else {
|
||
// Collect basic options only
|
||
options.template = 'default';
|
||
options.metadata = {};
|
||
options.variables = {};
|
||
options.toc = document.getElementById('basic-toc').checked;
|
||
options.tocDepth = 3;
|
||
options.numberSections = document.getElementById('basic-number-sections').checked;
|
||
options.citeproc = false;
|
||
}
|
||
|
||
if (advancedMode) {
|
||
// Collect custom template path
|
||
if (options.template === 'custom') {
|
||
options.template = document.getElementById('custom-template-path').value.trim();
|
||
}
|
||
|
||
// Collect metadata
|
||
const metadataFields = document.querySelectorAll('.metadata-field');
|
||
metadataFields.forEach(field => {
|
||
const key = field.querySelector('.metadata-key').value.trim();
|
||
const value = field.querySelector('.metadata-value').value.trim();
|
||
if (key && value) {
|
||
options.metadata[key] = value;
|
||
}
|
||
});
|
||
|
||
// PDF-specific options
|
||
if (currentExportFormat === 'pdf') {
|
||
options.pdfEngine = document.getElementById('pdf-engine').value;
|
||
const geometrySelect = document.getElementById('pdf-geometry');
|
||
if (geometrySelect.value === 'custom') {
|
||
options.geometry = document.getElementById('custom-geometry').value.trim() || 'margin=1in';
|
||
} else {
|
||
options.geometry = geometrySelect.value;
|
||
}
|
||
}
|
||
|
||
// Bibliography
|
||
const bibFile = document.getElementById('bibliography-file').value.trim();
|
||
const cslFile = document.getElementById('csl-file').value.trim();
|
||
if (bibFile) options.bibliography = bibFile;
|
||
if (cslFile) options.csl = cslFile;
|
||
} else {
|
||
// Basic mode - set default PDF options if needed
|
||
if (currentExportFormat === 'pdf') {
|
||
options.pdfEngine = 'xelatex';
|
||
options.geometry = 'margin=1in';
|
||
}
|
||
}
|
||
|
||
return options;
|
||
}
|
||
|
||
// Event listeners for export dialog
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
// Template selection
|
||
document.getElementById('export-template').addEventListener('change', (e) => {
|
||
const customPath = document.getElementById('custom-template-path');
|
||
const fileInput = document.getElementById('template-file-input');
|
||
|
||
if (e.target.value === 'custom') {
|
||
customPath.style.display = 'block';
|
||
fileInput.style.display = 'block';
|
||
} else {
|
||
customPath.style.display = 'none';
|
||
fileInput.style.display = 'none';
|
||
customPath.value = '';
|
||
}
|
||
});
|
||
|
||
// Advanced export toggle
|
||
document.getElementById('advanced-export-toggle').addEventListener('change', (e) => {
|
||
const advancedOptions = document.getElementById('advanced-export-options');
|
||
if (e.target.checked) {
|
||
advancedOptions.classList.remove('hidden');
|
||
// Scroll the advanced options into view after they become visible
|
||
setTimeout(() => {
|
||
advancedOptions.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||
}, 100);
|
||
} else {
|
||
advancedOptions.classList.add('hidden');
|
||
}
|
||
});
|
||
|
||
// Template file input
|
||
document.getElementById('template-file-input').addEventListener('change', (e) => {
|
||
const file = e.target.files[0];
|
||
if (file) {
|
||
document.getElementById('custom-template-path').value = file.path;
|
||
}
|
||
});
|
||
|
||
// PDF geometry selection
|
||
document.getElementById('pdf-geometry').addEventListener('change', (e) => {
|
||
const customGeometry = document.getElementById('custom-geometry');
|
||
if (e.target.value === 'custom') {
|
||
customGeometry.style.display = 'block';
|
||
} else {
|
||
customGeometry.style.display = 'none';
|
||
}
|
||
});
|
||
|
||
// Add metadata field
|
||
document.getElementById('add-metadata-field').addEventListener('click', () => {
|
||
const container = document.querySelector('.metadata-container');
|
||
const newField = document.createElement('div');
|
||
newField.className = 'metadata-field';
|
||
newField.innerHTML = `
|
||
<input type="text" placeholder="key" class="metadata-key">
|
||
<input type="text" placeholder="value" class="metadata-value">
|
||
`;
|
||
container.appendChild(newField);
|
||
});
|
||
|
||
// Browse bibliography
|
||
document.getElementById('browse-bibliography').addEventListener('click', () => {
|
||
const input = document.createElement('input');
|
||
input.type = 'file';
|
||
input.accept = '.bib,.yaml,.yml,.json';
|
||
input.onchange = (e) => {
|
||
const file = e.target.files[0];
|
||
if (file) {
|
||
document.getElementById('bibliography-file').value = file.path;
|
||
}
|
||
};
|
||
input.click();
|
||
});
|
||
|
||
// Browse CSL
|
||
document.getElementById('browse-csl').addEventListener('click', () => {
|
||
const input = document.createElement('input');
|
||
input.type = 'file';
|
||
input.accept = '.csl';
|
||
input.onchange = (e) => {
|
||
const file = e.target.files[0];
|
||
if (file) {
|
||
document.getElementById('csl-file').value = file.path;
|
||
}
|
||
};
|
||
input.click();
|
||
});
|
||
|
||
// Dialog close buttons
|
||
document.getElementById('export-dialog-close').addEventListener('click', hideExportDialog);
|
||
document.getElementById('export-cancel').addEventListener('click', hideExportDialog);
|
||
|
||
// Export confirm
|
||
document.getElementById('export-confirm').addEventListener('click', () => {
|
||
const options = collectExportOptions();
|
||
ipcRenderer.send('export-with-options', {
|
||
format: currentExportFormat,
|
||
options: options
|
||
});
|
||
hideExportDialog();
|
||
});
|
||
|
||
// Close on backdrop click
|
||
document.getElementById('export-dialog').addEventListener('click', (e) => {
|
||
if (e.target === document.getElementById('export-dialog')) {
|
||
hideExportDialog();
|
||
}
|
||
});
|
||
|
||
// Close on Escape key
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape' && !document.getElementById('export-dialog').classList.contains('hidden')) {
|
||
hideExportDialog();
|
||
}
|
||
});
|
||
});
|
||
|
||
// Batch Conversion Dialog functionality
|
||
let currentBatchOptions = {};
|
||
|
||
ipcRenderer.on('show-batch-dialog', () => {
|
||
showBatchDialog();
|
||
});
|
||
|
||
// ConvertAPI dialog handlers
|
||
ipcRenderer.on('show-convertapi-dialog', () => {
|
||
showConvertAPIDialog();
|
||
});
|
||
|
||
ipcRenderer.on('convertapi-status', (event, status) => {
|
||
document.getElementById('convertapi-status').textContent = status;
|
||
});
|
||
|
||
ipcRenderer.on('convertapi-complete', (event, result) => {
|
||
document.getElementById('convertapi-progress').classList.add('hidden');
|
||
if (result.success) {
|
||
document.getElementById('convertapi-dialog').classList.add('hidden');
|
||
}
|
||
});
|
||
|
||
ipcRenderer.on('batch-progress', (event, progress) => {
|
||
updateBatchProgress(progress);
|
||
});
|
||
|
||
ipcRenderer.on('folder-selected', (event, { type, path }) => {
|
||
if (type === 'input') {
|
||
document.getElementById('batch-input-folder').value = path;
|
||
} else if (type === 'output') {
|
||
document.getElementById('batch-output-folder').value = path;
|
||
}
|
||
validateBatchForm();
|
||
});
|
||
|
||
function showBatchDialog() {
|
||
const dialog = document.getElementById('batch-dialog');
|
||
dialog.classList.remove('hidden');
|
||
|
||
// Reset form
|
||
document.getElementById('batch-input-folder').value = '';
|
||
document.getElementById('batch-output-folder').value = '';
|
||
document.getElementById('batch-format').value = 'html';
|
||
document.getElementById('batch-include-subfolders').checked = true;
|
||
document.getElementById('batch-progress').classList.add('hidden');
|
||
document.getElementById('batch-start').disabled = true;
|
||
|
||
currentBatchOptions = {
|
||
template: 'default',
|
||
metadata: {},
|
||
variables: {},
|
||
toc: false,
|
||
tocDepth: 3,
|
||
numberSections: false,
|
||
citeproc: false
|
||
};
|
||
}
|
||
|
||
function hideBatchDialog() {
|
||
const dialog = document.getElementById('batch-dialog');
|
||
dialog.classList.add('hidden');
|
||
}
|
||
|
||
function updateBatchProgress(progress) {
|
||
const progressSection = document.getElementById('batch-progress');
|
||
const progressFill = document.getElementById('batch-progress-fill');
|
||
const progressText = document.getElementById('batch-progress-text');
|
||
const progressCount = document.getElementById('batch-progress-count');
|
||
|
||
progressSection.classList.remove('hidden');
|
||
|
||
const percentage = Math.round((progress.completed / progress.total) * 100);
|
||
progressFill.style.width = `${percentage}%`;
|
||
|
||
if (progress.completed === progress.total) {
|
||
progressText.textContent = 'Conversion complete!';
|
||
} else {
|
||
progressText.textContent = `Processing: ${progress.currentFile}`;
|
||
}
|
||
|
||
progressCount.textContent = `${progress.completed} / ${progress.total}`;
|
||
}
|
||
|
||
function validateBatchForm() {
|
||
const inputFolder = document.getElementById('batch-input-folder').value.trim();
|
||
const outputFolder = document.getElementById('batch-output-folder').value.trim();
|
||
const startButton = document.getElementById('batch-start');
|
||
|
||
startButton.disabled = !inputFolder || !outputFolder;
|
||
}
|
||
|
||
// Event listeners for batch dialog
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
// Browse input folder
|
||
document.getElementById('browse-input-folder').addEventListener('click', () => {
|
||
ipcRenderer.send('select-folder', 'input');
|
||
});
|
||
|
||
// Browse output folder
|
||
document.getElementById('browse-output-folder').addEventListener('click', () => {
|
||
ipcRenderer.send('select-folder', 'output');
|
||
});
|
||
|
||
// Show advanced options
|
||
document.getElementById('batch-show-options').addEventListener('click', () => {
|
||
const format = document.getElementById('batch-format').value;
|
||
currentExportFormat = format;
|
||
showExportDialog(format);
|
||
});
|
||
|
||
// Dialog close buttons
|
||
document.getElementById('batch-dialog-close').addEventListener('click', hideBatchDialog);
|
||
document.getElementById('batch-cancel').addEventListener('click', hideBatchDialog);
|
||
|
||
// Start batch conversion
|
||
document.getElementById('batch-start').addEventListener('click', () => {
|
||
const inputFolder = document.getElementById('batch-input-folder').value.trim();
|
||
const outputFolder = document.getElementById('batch-output-folder').value.trim();
|
||
const format = document.getElementById('batch-format').value;
|
||
|
||
if (!inputFolder || !outputFolder) {
|
||
return;
|
||
}
|
||
|
||
// Use current export options from advanced dialog if they were set
|
||
const options = currentBatchOptions;
|
||
|
||
// Start batch conversion
|
||
ipcRenderer.send('batch-convert', {
|
||
inputFolder,
|
||
outputFolder,
|
||
format,
|
||
options
|
||
});
|
||
|
||
// Show progress
|
||
document.getElementById('batch-progress').classList.remove('hidden');
|
||
document.getElementById('batch-start').disabled = true;
|
||
});
|
||
|
||
// Close on backdrop click
|
||
document.getElementById('batch-dialog').addEventListener('click', (e) => {
|
||
if (e.target === document.getElementById('batch-dialog')) {
|
||
hideBatchDialog();
|
||
}
|
||
});
|
||
|
||
// Close on Escape key (modified to handle both dialogs)
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') {
|
||
if (!document.getElementById('export-dialog').classList.contains('hidden')) {
|
||
hideExportDialog();
|
||
} else if (!document.getElementById('batch-dialog').classList.contains('hidden')) {
|
||
hideBatchDialog();
|
||
}
|
||
}
|
||
});
|
||
|
||
// Input validation
|
||
document.getElementById('batch-input-folder').addEventListener('input', validateBatchForm);
|
||
document.getElementById('batch-output-folder').addEventListener('input', validateBatchForm);
|
||
});
|
||
|
||
// Override the export dialog confirm to also save batch options
|
||
const originalExportConfirm = document.getElementById('export-confirm');
|
||
if (originalExportConfirm) {
|
||
originalExportConfirm.addEventListener('click', () => {
|
||
// If batch dialog is open, save options for batch conversion
|
||
if (!document.getElementById('batch-dialog').classList.contains('hidden')) {
|
||
currentBatchOptions = collectExportOptions();
|
||
}
|
||
});
|
||
}
|
||
|
||
// ConvertAPI Dialog Functions
|
||
function showConvertAPIDialog() {
|
||
const dialog = document.getElementById('convertapi-dialog');
|
||
dialog.classList.remove('hidden');
|
||
|
||
// Load saved API key if exists
|
||
const savedKey = localStorage.getItem('convertapi-key');
|
||
if (savedKey) {
|
||
document.getElementById('convertapi-key').value = savedKey;
|
||
}
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
// ConvertAPI dialog close
|
||
const convertAPIDialogClose = document.getElementById('convertapi-dialog-close');
|
||
if (convertAPIDialogClose) {
|
||
convertAPIDialogClose.addEventListener('click', () => {
|
||
document.getElementById('convertapi-dialog').classList.add('hidden');
|
||
});
|
||
}
|
||
|
||
// ConvertAPI cancel
|
||
const convertAPICancel = document.getElementById('convertapi-cancel');
|
||
if (convertAPICancel) {
|
||
convertAPICancel.addEventListener('click', () => {
|
||
document.getElementById('convertapi-dialog').classList.add('hidden');
|
||
});
|
||
}
|
||
|
||
// ConvertAPI convert
|
||
const convertAPIConvert = document.getElementById('convertapi-convert');
|
||
if (convertAPIConvert) {
|
||
convertAPIConvert.addEventListener('click', () => {
|
||
const apiKey = document.getElementById('convertapi-key').value;
|
||
const fromFormat = document.getElementById('convertapi-from').value;
|
||
const toFormat = document.getElementById('convertapi-to').value;
|
||
const filePath = tabManager.getCurrentFilePath();
|
||
|
||
if (!apiKey) {
|
||
alert('Please enter your ConvertAPI key');
|
||
return;
|
||
}
|
||
|
||
if (!filePath) {
|
||
alert('Please save the file first');
|
||
return;
|
||
}
|
||
|
||
// Save API key for future use
|
||
localStorage.setItem('convertapi-key', apiKey);
|
||
|
||
// Show progress
|
||
document.getElementById('convertapi-progress').classList.remove('hidden');
|
||
|
||
// Send conversion request
|
||
ipcRenderer.send('convertapi-convert', {
|
||
apiKey,
|
||
fromFormat,
|
||
toFormat,
|
||
filePath
|
||
});
|
||
});
|
||
}
|
||
});
|
||
|
||
// IPC event listeners for recent files functionality
|
||
ipcRenderer.on('recent-files-cleared', () => {
|
||
if (tabManager) {
|
||
tabManager.recentFiles = [];
|
||
localStorage.setItem('recentFiles', JSON.stringify([]));
|
||
console.log('Recent files cleared');
|
||
}
|
||
});
|
||
|
||
// Add math rendering support using KaTeX for enhanced preview
|
||
function initMathSupport() {
|
||
// Add KaTeX CSS
|
||
const katexCSS = document.createElement('link');
|
||
katexCSS.rel = 'stylesheet';
|
||
katexCSS.href = 'https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.css';
|
||
katexCSS.crossOrigin = 'anonymous';
|
||
document.head.appendChild(katexCSS);
|
||
|
||
// Add KaTeX JS
|
||
const katexJS = document.createElement('script');
|
||
katexJS.src = 'https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.js';
|
||
katexJS.crossOrigin = 'anonymous';
|
||
katexJS.onload = () => {
|
||
// Add auto-render extension
|
||
const autoRenderJS = document.createElement('script');
|
||
autoRenderJS.src = 'https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/contrib/auto-render.min.js';
|
||
autoRenderJS.crossOrigin = 'anonymous';
|
||
autoRenderJS.onload = () => {
|
||
console.log('Math support (KaTeX) initialized');
|
||
// Re-render current preview to include math
|
||
if (tabManager) {
|
||
tabManager.updatePreview();
|
||
}
|
||
};
|
||
document.head.appendChild(autoRenderJS);
|
||
};
|
||
document.head.appendChild(katexJS);
|
||
}
|
||
|
||
// Initialize math support on load
|
||
initMathSupport(); |