feat: add custom print preview dialog with configurable options

This commit is contained in:
2026-03-04 16:04:51 +05:30
parent 9348b2bd1d
commit ca0b250506
6 changed files with 357 additions and 24 deletions
+75
View File
@@ -331,6 +331,81 @@
</div> </div>
</div> </div>
<!-- Print Preview Dialog -->
<div class="export-dialog hidden" id="print-preview-overlay">
<div class="export-dialog-content print-preview-dialog">
<div class="export-dialog-header">
<h3>Print Preview</h3>
<button id="print-preview-close" title="Close" style="background:none;border:none;color:white;font-size:24px;cursor:pointer;">&times;</button>
</div>
<div class="print-preview-body">
<div class="print-preview-sidebar">
<div class="print-option-group">
<label>Paper Size</label>
<select id="print-paper-size">
<option value="A3">A3</option>
<option value="A4" selected>A4</option>
<option value="A5">A5</option>
<option value="Letter">Letter</option>
<option value="Legal">Legal</option>
<option value="Tabloid">Tabloid</option>
</select>
</div>
<div class="print-option-group">
<label>Orientation</label>
<select id="print-orientation">
<option value="portrait" selected>Portrait</option>
<option value="landscape">Landscape</option>
</select>
</div>
<div class="print-option-group">
<label>Margins</label>
<select id="print-margins">
<option value="default" selected>Default</option>
<option value="narrow">Narrow</option>
<option value="wide">Wide</option>
<option value="none">None</option>
</select>
</div>
<div class="print-option-group">
<label>Scale</label>
<div class="scale-control">
<input type="range" id="print-scale" min="50" max="200" value="100">
<span id="print-scale-value">100%</span>
</div>
</div>
<div class="print-option-group">
<label class="checkbox-label">
<input type="checkbox" id="print-headers" checked>
Headers &amp; Footers
</label>
</div>
<div class="print-option-group">
<label class="checkbox-label">
<input type="checkbox" id="print-background" checked>
Background Colors
</label>
</div>
<div class="print-option-group">
<label>Pages</label>
<select id="print-pages">
<option value="all" selected>All</option>
<option value="custom">Custom Range</option>
</select>
<input type="text" id="print-page-range" placeholder="e.g., 1-3, 5" class="hidden print-range-input">
</div>
<div class="print-actions">
<button class="btn-primary" id="print-execute">Print</button>
<button class="btn-secondary" id="print-cancel">Cancel</button>
</div>
</div>
<div class="print-preview-content">
<iframe id="print-preview-frame" style="width:100%;height:100%;border:none;background:white;"></iframe>
</div>
</div>
</div>
</div>
<!-- Command Palette --> <!-- Command Palette -->
<div class="command-palette-overlay hidden" id="command-palette-overlay"> <div class="command-palette-overlay hidden" id="command-palette-overlay">
<div class="command-palette"> <div class="command-palette">
+26
View File
@@ -2472,6 +2472,32 @@ ipcMain.on('do-print', (event, { withStyles }) => {
} }
}); });
// Handle printing with custom options from print preview dialog
ipcMain.on('do-print-with-options', (event, options) => {
if (!mainWindow) return;
const marginsMap = {
'default': { marginType: 'default' },
'narrow': { top: 0.4, bottom: 0.4, left: 0.4, right: 0.4 },
'wide': { top: 1.0, bottom: 1.0, left: 1.0, right: 1.0 },
'none': { top: 0, bottom: 0, left: 0, right: 0 },
};
const printOptions = {
silent: false,
printBackground: options.background,
landscape: options.orientation === 'landscape',
scaleFactor: options.scale / 100,
pageSize: options.paperSize,
};
if (options.margins && options.margins !== 'default') {
printOptions.margins = marginsMap[options.margins];
}
mainWindow.webContents.print(printOptions);
});
// Handle renderer ready for file association // Handle renderer ready for file association
ipcMain.on('renderer-ready', (event) => { ipcMain.on('renderer-ready', (event) => {
console.log('[MAIN] renderer-ready received, rendererReady was:', rendererReady); console.log('[MAIN] renderer-ready received, rendererReady was:', rendererReady);
+1
View File
@@ -29,6 +29,7 @@ const ALLOWED_SEND_CHANNELS = [
// Print // Print
'do-print', 'do-print',
'do-print-with-options',
// Export // Export
'export-with-options', 'export-with-options',
+139
View File
@@ -0,0 +1,139 @@
class PrintPreview {
constructor() {
this.overlay = document.getElementById('print-preview-overlay');
this._lastContent = '';
this.setupEventListeners();
}
open(htmlContent) {
this._lastContent = htmlContent;
this.overlay.classList.remove('hidden');
this.updatePreview(htmlContent);
this.updateScaleLabel();
}
close() {
this.overlay.classList.add('hidden');
}
setupEventListeners() {
document.getElementById('print-preview-close')?.addEventListener('click', () => this.close());
document.getElementById('print-cancel')?.addEventListener('click', () => this.close());
document.getElementById('print-execute')?.addEventListener('click', () => this.executePrint());
// Update preview on option changes
['print-paper-size', 'print-orientation', 'print-margins'].forEach(id => {
document.getElementById(id)?.addEventListener('change', () => this.refreshPreview());
});
// Scale slider
const scaleSlider = document.getElementById('print-scale');
scaleSlider?.addEventListener('input', () => this.updateScaleLabel());
// Page range toggle
document.getElementById('print-pages')?.addEventListener('change', (e) => {
const rangeInput = document.getElementById('print-page-range');
if (rangeInput) {
rangeInput.classList.toggle('hidden', e.target.value !== 'custom');
}
});
// Close on overlay click
this.overlay?.addEventListener('click', (e) => {
if (e.target === this.overlay) this.close();
});
// Close on Escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !this.overlay.classList.contains('hidden')) {
this.close();
}
});
}
updateScaleLabel() {
const scale = document.getElementById('print-scale')?.value || 100;
const label = document.getElementById('print-scale-value');
if (label) label.textContent = `${scale}%`;
}
updatePreview(htmlContent) {
const frame = document.getElementById('print-preview-frame');
if (!frame) return;
this._lastContent = htmlContent;
const orientation = document.getElementById('print-orientation')?.value || 'portrait';
const paperSize = document.getElementById('print-paper-size')?.value || 'A4';
// Get dimensions for paper size
const sizes = {
'A3': { width: '297mm', height: '420mm' },
'A4': { width: '210mm', height: '297mm' },
'A5': { width: '148mm', height: '210mm' },
'Letter': { width: '8.5in', height: '11in' },
'Legal': { width: '8.5in', height: '14in' },
'Tabloid': { width: '11in', height: '17in' },
};
const size = sizes[paperSize] || sizes['A4'];
const width = orientation === 'landscape' ? size.height : size.width;
const height = orientation === 'landscape' ? size.width : size.height;
const previewHtml = `
<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px;
line-height: 1.6;
}
@page { size: ${width} ${height}; }
pre { background: #f5f5f5; padding: 12px; border-radius: 6px; overflow-x: auto; }
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
pre code { background: none; padding: 0; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; }
blockquote { border-left: 4px solid #ddd; margin-left: 0; padding-left: 16px; color: #666; }
img { max-width: 100%; }
h1, h2, h3 { margin-top: 1.5em; }
</style>
</head>
<body>${htmlContent || ''}</body>
</html>
`;
frame.srcdoc = previewHtml;
}
refreshPreview() {
if (this._lastContent) {
this.updatePreview(this._lastContent);
}
}
getOptions() {
return {
paperSize: document.getElementById('print-paper-size')?.value || 'A4',
orientation: document.getElementById('print-orientation')?.value || 'portrait',
margins: document.getElementById('print-margins')?.value || 'default',
scale: parseInt(document.getElementById('print-scale')?.value || '100'),
headers: document.getElementById('print-headers')?.checked ?? true,
background: document.getElementById('print-background')?.checked ?? true,
pages: document.getElementById('print-pages')?.value || 'all',
pageRange: document.getElementById('print-page-range')?.value || '',
};
}
executePrint() {
const options = this.getOptions();
const { ipcRenderer } = require('electron');
ipcRenderer.send('do-print-with-options', options);
this.close();
}
}
module.exports = { PrintPreview };
+15 -24
View File
@@ -12,6 +12,7 @@ const { createEditor } = require('./editor/codemirror-setup');
const { undo, redo } = require('@codemirror/commands'); const { undo, redo } = require('@codemirror/commands');
const { SidebarManager } = require('./sidebar/sidebar-manager'); const { SidebarManager } = require('./sidebar/sidebar-manager');
const { CommandPalette } = require('./command-palette'); const { CommandPalette } = require('./command-palette');
const { PrintPreview } = require('./print-preview');
// Configure marked with highlight extension // Configure marked with highlight extension
marked.use(markedHighlight({ marked.use(markedHighlight({
@@ -1051,6 +1052,9 @@ document.addEventListener('DOMContentLoaded', () => {
// Initialize command palette // Initialize command palette
const commandPalette = new CommandPalette(); const commandPalette = new CommandPalette();
// Initialize print preview
const printPreview = new PrintPreview();
// Register commands // Register commands
commandPalette.register('New File', 'Ctrl+N', () => tabManager.createNewTab()); commandPalette.register('New File', 'Ctrl+N', () => tabManager.createNewTab());
commandPalette.register('Open File', 'Ctrl+O', () => ipcRenderer.send('menu-open')); commandPalette.register('Open File', 'Ctrl+O', () => ipcRenderer.send('menu-open'));
@@ -1087,6 +1091,11 @@ document.addEventListener('DOMContentLoaded', () => {
commandPalette.register('Toggle Sidebar: Git', 'Ctrl+Shift+G', () => sidebarManager.togglePanel('git')); commandPalette.register('Toggle Sidebar: Git', 'Ctrl+Shift+G', () => sidebarManager.togglePanel('git'));
commandPalette.register('Toggle Sidebar: Snippets', '', () => sidebarManager.togglePanel('snippets')); commandPalette.register('Toggle Sidebar: Snippets', '', () => sidebarManager.togglePanel('snippets'));
commandPalette.register('Toggle Sidebar: Templates', '', () => sidebarManager.togglePanel('templates')); commandPalette.register('Toggle Sidebar: Templates', '', () => sidebarManager.togglePanel('templates'));
commandPalette.register('Print Preview', 'Ctrl+P', () => {
const tab = tabManager.tabs.get(tabManager.activeTabId);
const preview = document.getElementById(`preview-${tab.id}`);
printPreview.open(preview?.innerHTML || '');
});
commandPalette.register('Export to PDF', '', () => ipcRenderer.send('export', 'pdf')); commandPalette.register('Export to PDF', '', () => ipcRenderer.send('export', 'pdf'));
commandPalette.register('Export to DOCX', '', () => ipcRenderer.send('export', 'docx')); commandPalette.register('Export to DOCX', '', () => ipcRenderer.send('export', 'docx'));
commandPalette.register('Export to HTML', '', () => ipcRenderer.send('export', 'html')); commandPalette.register('Export to HTML', '', () => ipcRenderer.send('export', 'html'));
@@ -1261,18 +1270,18 @@ ipcRenderer.on('adjust-font-size', (event, action) => {
updateFontSizes(currentFontSize); updateFontSizes(currentFontSize);
}); });
// Print preview request handlers - handle printing directly // Print preview request handlers - open print preview dialog
ipcRenderer.on('print-preview', () => { ipcRenderer.on('print-preview', () => {
console.log('[RENDERER] print-preview received'); console.log('[RENDERER] print-preview received');
handlePrintPreview(false); openPrintPreviewDialog();
}); });
ipcRenderer.on('print-preview-styled', () => { ipcRenderer.on('print-preview-styled', () => {
console.log('[RENDERER] print-preview-styled received'); console.log('[RENDERER] print-preview-styled received');
handlePrintPreview(true); openPrintPreviewDialog();
}); });
function handlePrintPreview(withStyles) { function openPrintPreviewDialog() {
const activeTabId = tabManager ? tabManager.activeTabId : 1; const activeTabId = tabManager ? tabManager.activeTabId : 1;
const previewContent = document.getElementById(`preview-${activeTabId}`); const previewContent = document.getElementById(`preview-${activeTabId}`);
@@ -1281,26 +1290,8 @@ function handlePrintPreview(withStyles) {
return; return;
} }
console.log('[RENDERER] Starting print with withStyles:', withStyles); const printPreviewInstance = new PrintPreview();
console.log('[RENDERER] Preview content length:', previewContent.innerHTML.length); printPreviewInstance.open(previewContent.innerHTML);
// Add body classes for print mode - let CSS handle everything
document.body.classList.add('printing');
if (!withStyles) {
document.body.classList.add('printing-no-styles');
}
// Wait for CSS to apply then print
setTimeout(() => {
console.log('[RENDERER] Sending do-print to main process');
ipcRenderer.send('do-print', { withStyles });
// Restore classes after print dialog opens
setTimeout(() => {
document.body.classList.remove('printing', 'printing-no-styles');
console.log('[RENDERER] Print classes removed');
}, 1000);
}, 100);
} }
// Export Dialog functionality // Export Dialog functionality
+101
View File
@@ -2232,3 +2232,104 @@ body[class*="dark"] .breadcrumb-bar {
color: #888; color: #888;
border-color: #333; border-color: #333;
} }
/* Print Preview Dialog */
.print-preview-dialog {
width: 90vw;
max-width: 1200px;
height: 80vh;
display: flex;
flex-direction: column;
}
.print-preview-body {
display: flex;
flex: 1;
overflow: hidden;
}
.print-preview-sidebar {
width: 260px;
padding: 16px;
border-right: 1px solid var(--gray-200, #e5e7eb);
overflow-y: auto;
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 16px;
}
.print-preview-content {
flex: 1;
padding: 16px;
background: #e5e7eb;
display: flex;
justify-content: center;
overflow: auto;
}
.print-preview-content iframe {
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
border-radius: 4px;
}
.print-option-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.print-option-group label {
font-size: 13px;
font-weight: 500;
color: var(--gray-600, #4b5563);
}
.print-option-group select,
.print-option-group input[type="text"] {
padding: 8px 10px;
border: 1px solid var(--gray-300, #d1d5db);
border-radius: 8px;
font-size: 13px;
background: white;
}
.scale-control {
display: flex;
align-items: center;
gap: 8px;
}
.scale-control input[type="range"] {
flex: 1;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.print-range-input {
margin-top: 6px;
}
.print-actions {
display: flex;
gap: 8px;
margin-top: auto;
padding-top: 16px;
}
.btn-primary {
flex: 1;
padding: 10px;
background: var(--primary-dark, #5661b3);
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
}
.btn-primary:hover {
opacity: 0.9;
}
.btn-secondary {
flex: 1;
padding: 10px;
background: var(--gray-100, #f3f4f6);
color: var(--gray-700, #374151);
border: 1px solid var(--gray-300, #d1d5db);
border-radius: 8px;
font-size: 14px;
cursor: pointer;
}