T21: Add PrintPreview.tsx component
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import type { YjsDocument } from '../../crdt/YjsDocument';
|
||||
import {
|
||||
preparePrint,
|
||||
printPages,
|
||||
svgToDataUrl,
|
||||
DEFAULT_PRINT_SETTINGS,
|
||||
type PrintSettings,
|
||||
type PrintResult,
|
||||
type PageSize,
|
||||
type Orientation,
|
||||
type ScalePreset,
|
||||
} from '../../services/printService';
|
||||
import './PrintPreview.css';
|
||||
|
||||
interface PrintPreviewProps {
|
||||
yjsDoc: React.RefObject<YjsDocument | null>;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const PAGE_SIZES: PageSize[] = ['a4', 'a3', 'a2', 'a1'];
|
||||
const SCALE_PRESETS: ScalePreset[] = ['1:50', '1:100', '1:200', 'custom'];
|
||||
|
||||
const PrintPreview: React.FC<PrintPreviewProps> = ({ yjsDoc, onClose }) => {
|
||||
const [settings, setSettings] = useState<PrintSettings>({ ...DEFAULT_PRINT_SETTINGS });
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
|
||||
const printResult: PrintResult | null = useMemo(() => {
|
||||
const doc = yjsDoc?.current;
|
||||
if (!doc) return null;
|
||||
return preparePrint(doc, settings);
|
||||
}, [yjsDoc, settings]);
|
||||
|
||||
const updateSettings = useCallback((updates: Partial<PrintSettings>) => {
|
||||
setSettings((prev) => ({ ...prev, ...updates }));
|
||||
setCurrentPage(0);
|
||||
}, []);
|
||||
|
||||
const updateTitleBlock = useCallback((updates: Partial<NonNullable<PrintSettings['titleBlockData']>>) => {
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
titleBlockData: { ...prev.titleBlockData!, ...updates },
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handlePrint = useCallback(() => {
|
||||
if (printResult) {
|
||||
printPages(printResult);
|
||||
}
|
||||
}, [printResult]);
|
||||
|
||||
if (!printResult) {
|
||||
return (
|
||||
<div className="print-preview-overlay" role="dialog" aria-modal="true" aria-label="Print Preview">
|
||||
<div className="print-preview-modal">
|
||||
<div className="print-preview-header">
|
||||
<h2>Druckvorschau</h2>
|
||||
<button className="print-preview-close" onClick={onClose} aria-label="Close">✕</button>
|
||||
</div>
|
||||
<div className="print-preview-empty">Kein Dokument verfügbar.</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { layout, pages, bbox } = printResult;
|
||||
const page = pages[currentPage];
|
||||
const previewUrl = page ? svgToDataUrl(page.svg) : '';
|
||||
|
||||
return (
|
||||
<div className="print-preview-overlay" role="dialog" aria-modal="true" aria-label="Print Preview">
|
||||
<div className="print-preview-modal">
|
||||
{/* Header */}
|
||||
<div className="print-preview-header">
|
||||
<h2>Druckvorschau</h2>
|
||||
<button className="print-preview-close" onClick={onClose} aria-label="Close">✕</button>
|
||||
</div>
|
||||
|
||||
<div className="print-preview-body">
|
||||
{/* Settings sidebar */}
|
||||
<div className="print-preview-settings">
|
||||
<fieldset className="print-settings-fieldset">
|
||||
<legend>Seite</legend>
|
||||
<label className="print-settings-label">
|
||||
Format
|
||||
<select
|
||||
className="print-settings-select"
|
||||
value={settings.pageSize}
|
||||
onChange={(e) => updateSettings({ pageSize: e.target.value as PageSize })}
|
||||
>
|
||||
{PAGE_SIZES.map((s) => (
|
||||
<option key={s} value={s}>{s.toUpperCase()}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="print-settings-label">
|
||||
Ausrichtung
|
||||
<select
|
||||
className="print-settings-select"
|
||||
value={settings.orientation}
|
||||
onChange={(e) => updateSettings({ orientation: e.target.value as Orientation })}
|
||||
>
|
||||
<option value="portrait">Hochformat</option>
|
||||
<option value="landscape">Querformat</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="print-settings-label">
|
||||
Rand (mm)
|
||||
<input
|
||||
type="number"
|
||||
className="print-settings-input"
|
||||
value={settings.margin}
|
||||
min="0"
|
||||
max="50"
|
||||
onChange={(e) => updateSettings({ margin: parseFloat(e.target.value) || 0 })}
|
||||
/>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="print-settings-fieldset">
|
||||
<legend>Maßstab</legend>
|
||||
<label className="print-settings-label">
|
||||
Vorwahl
|
||||
<select
|
||||
className="print-settings-select"
|
||||
value={settings.scale}
|
||||
onChange={(e) => updateSettings({ scale: e.target.value as ScalePreset })}
|
||||
>
|
||||
{SCALE_PRESETS.map((s) => (
|
||||
<option key={s} value={s}>{s}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
{settings.scale === 'custom' && (
|
||||
<label className="print-settings-label">
|
||||
Custom (1:N)
|
||||
<input
|
||||
type="number"
|
||||
className="print-settings-input"
|
||||
value={settings.customScale}
|
||||
min="1"
|
||||
onChange={(e) => updateSettings({ customScale: parseFloat(e.target.value) || 1 })}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="print-settings-fieldset">
|
||||
<legend>Optionen</legend>
|
||||
<label className="print-settings-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.showBorder}
|
||||
onChange={(e) => updateSettings({ showBorder: e.target.checked })}
|
||||
/>
|
||||
Druckrand
|
||||
</label>
|
||||
<label className="print-settings-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.showTitleBlock}
|
||||
onChange={(e) => updateSettings({ showTitleBlock: e.target.checked })}
|
||||
/>
|
||||
Titelblock
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
{settings.showTitleBlock && settings.titleBlockData && (
|
||||
<fieldset className="print-settings-fieldset">
|
||||
<legend>Titelblock</legend>
|
||||
<label className="print-settings-label">
|
||||
Titel
|
||||
<input
|
||||
type="text"
|
||||
className="print-settings-text-input"
|
||||
value={settings.titleBlockData.title}
|
||||
onChange={(e) => updateTitleBlock({ title: e.target.value })}
|
||||
/>
|
||||
</label>
|
||||
<label className="print-settings-label">
|
||||
Autor
|
||||
<input
|
||||
type="text"
|
||||
className="print-settings-text-input"
|
||||
value={settings.titleBlockData.author}
|
||||
onChange={(e) => updateTitleBlock({ author: e.target.value })}
|
||||
/>
|
||||
</label>
|
||||
<label className="print-settings-label">
|
||||
Datum
|
||||
<input
|
||||
type="text"
|
||||
className="print-settings-text-input"
|
||||
value={settings.titleBlockData.date}
|
||||
onChange={(e) => updateTitleBlock({ date: e.target.value })}
|
||||
/>
|
||||
</label>
|
||||
</fieldset>
|
||||
)}
|
||||
|
||||
{/* Info */}
|
||||
<div className="print-preview-info">
|
||||
<div className="print-info-row">
|
||||
<span>Zeichnung:</span>
|
||||
<span>{Math.round(bbox.width)} × {Math.round(bbox.height)} CAD</span>
|
||||
</div>
|
||||
<div className="print-info-row">
|
||||
<span>Seiten:</span>
|
||||
<span>{layout.totalPages} ({layout.cols} × {layout.rows})</span>
|
||||
</div>
|
||||
<div className="print-info-row">
|
||||
<span>Format:</span>
|
||||
<span>{layout.pageWidth} × {layout.pageHeight} mm</span>
|
||||
</div>
|
||||
<div className="print-info-row">
|
||||
<span>Maßstab:</span>
|
||||
<span>1:{layout.scaleValue}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview area */}
|
||||
<div className="print-preview-canvas">
|
||||
{pages.length > 0 && previewUrl ? (
|
||||
<>
|
||||
<div className="print-preview-page-container">
|
||||
<img
|
||||
src={previewUrl}
|
||||
alt={`Page ${currentPage + 1}`}
|
||||
className="print-preview-page-image"
|
||||
/>
|
||||
<div className="print-preview-page-label">
|
||||
Seite {currentPage + 1} / {pages.length}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Page navigation */}
|
||||
{pages.length > 1 && (
|
||||
<div className="print-preview-nav">
|
||||
<button
|
||||
className="print-preview-nav-btn"
|
||||
onClick={() => setCurrentPage(Math.max(0, currentPage - 1))}
|
||||
disabled={currentPage === 0}
|
||||
>
|
||||
← Zurück
|
||||
</button>
|
||||
<span className="print-preview-nav-info">
|
||||
{currentPage + 1} / {pages.length}
|
||||
</span>
|
||||
<button
|
||||
className="print-preview-nav-btn"
|
||||
onClick={() => setCurrentPage(Math.min(pages.length - 1, currentPage + 1))}
|
||||
disabled={currentPage === pages.length - 1}
|
||||
>
|
||||
Weiter →
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Page grid overview */}
|
||||
{pages.length > 1 && (
|
||||
<div className="print-preview-grid">
|
||||
{pages.map((p, i) => (
|
||||
<button
|
||||
key={i}
|
||||
className={`print-preview-grid-cell ${i === currentPage ? 'active' : ''}`}
|
||||
onClick={() => setCurrentPage(i)}
|
||||
title={`Seite ${i + 1} (Spalte ${p.col + 1}, Zeile ${p.row + 1})}`}
|
||||
>
|
||||
{i + 1}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="print-preview-empty">Keine Elemente zum Drucken.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer with actions */}
|
||||
<div className="print-preview-footer">
|
||||
<button className="print-preview-btn print-preview-btn-secondary" onClick={onClose}>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
className="print-preview-btn print-preview-btn-primary"
|
||||
onClick={handlePrint}
|
||||
disabled={pages.length === 0}
|
||||
>
|
||||
🖨️ Drucken
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PrintPreview;
|
||||
Reference in New Issue
Block a user