task(G5): browser print stylesheet - @media print A4 landscape, screen-only chrome hidden, print-area fill, print service with cleanup
This commit is contained in:
@@ -10,6 +10,7 @@ import React, { useCallback, useEffect, useState } from 'react';
|
|||||||
import type { LayoutDefinition } from '../types/cad.types';
|
import type { LayoutDefinition } from '../types/cad.types';
|
||||||
import { getActiveDocument, onActiveDocumentChanged } from '../kernel/document/documentService';
|
import { getActiveDocument, onActiveDocumentChanged } from '../kernel/document/documentService';
|
||||||
import { insertTitleBlockForLayout } from '../services/titleBlockService';
|
import { insertTitleBlockForLayout } from '../services/titleBlockService';
|
||||||
|
import { printLayout } from '../services/printService';
|
||||||
|
|
||||||
export interface LayoutView {
|
export interface LayoutView {
|
||||||
center: { x: number; y: number };
|
center: { x: number; y: number };
|
||||||
@@ -120,6 +121,10 @@ const LayoutBar: React.FC<LayoutBarProps> = ({ doc: docProp, bridge: bridgeProp
|
|||||||
reload();
|
reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePrint = () => {
|
||||||
|
void printLayout();
|
||||||
|
};
|
||||||
|
|
||||||
const handleZoomTo = (l: LayoutDefinition) => {
|
const handleZoomTo = (l: LayoutDefinition) => {
|
||||||
bridge?.zoomTo(l.viewport.center, l.viewport.scale);
|
bridge?.zoomTo(l.viewport.center, l.viewport.scale);
|
||||||
};
|
};
|
||||||
@@ -137,6 +142,12 @@ const LayoutBar: React.FC<LayoutBarProps> = ({ doc: docProp, bridge: bridgeProp
|
|||||||
onClick={handleAdd}
|
onClick={handleAdd}
|
||||||
disabled={!bridge || !doc}
|
disabled={!bridge || !doc}
|
||||||
>+</button>
|
>+</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="layout-bar-print screen-only"
|
||||||
|
title="Layout drucken (A4 landscape)"
|
||||||
|
onClick={handlePrint}
|
||||||
|
>🖨</button>
|
||||||
</div>
|
</div>
|
||||||
{layouts.length === 0 ? (
|
{layouts.length === 0 ? (
|
||||||
<div className="layout-bar-empty">Keine Layouts</div>
|
<div className="layout-bar-empty">Keine Layouts</div>
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Task G5 – Browser-Print-Service.
|
||||||
|
*
|
||||||
|
* Bereitet die App für window.print() vor: Der Body erhält die Klasse
|
||||||
|
* `print-layout`, das @media print Stylesheet blendet das UI-Chrome aus
|
||||||
|
* und zeigt nur den Druckbereich (Canvas/Layout). Nach dem Druck wird
|
||||||
|
* die Klasse wieder entfernt (kein DOM-Mutation-Restzustand).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const PRINT_CLASS = 'print-layout';
|
||||||
|
|
||||||
|
/** Setzt den Print-Modus (Body-Klasse; @media print regelt das Rendering). */
|
||||||
|
export function preparePrintLayout(): void {
|
||||||
|
document.body.classList.add(PRINT_CLASS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Entfernt den Print-Modus (nach Druck oder bei Abbruch). */
|
||||||
|
export function cleanupPrintLayout(): void {
|
||||||
|
document.body.classList.remove(PRINT_CLASS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Druck-Flow: Vorbereitung → Rendern der nächsten Frames (Styles müssen
|
||||||
|
* greifen, bevor der Browser den Print-Dialog aufbaut) → window.print
|
||||||
|
* → Aufräumen. await-fähig für Tests (window.print wird gemockt).
|
||||||
|
*/
|
||||||
|
export async function printLayout(): Promise<void> {
|
||||||
|
preparePrintLayout();
|
||||||
|
// Zwei Frames warten: Style-Anwendung + Layout der Print-Ansicht
|
||||||
|
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
||||||
|
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
||||||
|
try {
|
||||||
|
window.print();
|
||||||
|
} finally {
|
||||||
|
cleanupPrintLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2925,3 +2925,52 @@ body.drawer-open { overflow: hidden; }
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
.layout-bar-tb:hover { color: #4a90d9; background: rgba(74, 144, 217, 0.15); }
|
.layout-bar-tb:hover { color: #4a90d9; background: rgba(74, 144, 217, 0.15); }
|
||||||
|
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
Task G5 – Browser-Print: @media print zeigt nur den Druckbereich
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
@media print {
|
||||||
|
@page {
|
||||||
|
size: A4 landscape;
|
||||||
|
margin: 10mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* UI-Chrome im Druck ausblenden */
|
||||||
|
.screen-only {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Druckbereich füllt die Seite */
|
||||||
|
.print-area {
|
||||||
|
width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Farben/Linien so drucken wie angezeigt */
|
||||||
|
.print-layout * {
|
||||||
|
-webkit-print-color-adjust: exact !important;
|
||||||
|
print-color-adjust: exact !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Body: kein Scroll, weiße Fläche */
|
||||||
|
body {
|
||||||
|
background: #fff !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-bar-print {
|
||||||
|
background: transparent;
|
||||||
|
color: #6b7280;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.layout-bar-print:hover { color: #4a90d9; background: rgba(74, 144, 217, 0.15); }
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* Task G5 – Browser-Print: @media print Stylesheet + Print-Service.
|
||||||
|
*
|
||||||
|
* printService bereitet den Body auf den Druck vor (print-layout-Klasse,
|
||||||
|
* Print-Modus ohne UI-Chrome), printLayout() triggert window.print nach
|
||||||
|
* Vorbereitung und räumt danach ab. Die CSS-Datei muss einen @media print
|
||||||
|
* Block mit den Kernregeln (UI ausblenden, Canvas füllen, @page)
|
||||||
|
* enthalten — verifiziert als Datei-Assertion (jsdom kann kein print-CSS
|
||||||
|
* rendern).
|
||||||
|
*/
|
||||||
|
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { preparePrintLayout, cleanupPrintLayout, printLayout } from '../src/services/printService';
|
||||||
|
|
||||||
|
describe('G5: printService', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupPrintLayout();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preparePrintLayout setzt print-layout-Klasse am body', () => {
|
||||||
|
document.body.classList.remove('print-layout');
|
||||||
|
preparePrintLayout();
|
||||||
|
expect(document.body.classList.contains('print-layout')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cleanupPrintLayout entfernt die Klasse wieder', () => {
|
||||||
|
preparePrintLayout();
|
||||||
|
cleanupPrintLayout();
|
||||||
|
expect(document.body.classList.contains('print-layout')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('printLayout ruft window.print nach Vorbereitung und räumt ab (async)', async () => {
|
||||||
|
const printSpy = vi.spyOn(window, 'print').mockImplementation(() => {});
|
||||||
|
await printLayout();
|
||||||
|
expect(printSpy).toHaveBeenCalledTimes(1);
|
||||||
|
// nach dem Druck ist wieder aufgeräumt
|
||||||
|
expect(document.body.classList.contains('print-layout')).toBe(false);
|
||||||
|
printSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('G5: Print-Stylesheet-Regeln (Datei-Assertion)', () => {
|
||||||
|
const css = readFileSync(join(__dirname, '..', 'src', 'styles.css'), 'utf-8');
|
||||||
|
const printBlock = css.slice(css.indexOf('@media print'));
|
||||||
|
|
||||||
|
it('enthält einen @media print Block mit @page A4 landscape', () => {
|
||||||
|
expect(css).toContain('@media print');
|
||||||
|
expect(printBlock).toMatch(/@page\s*\{[^}]*size:\s*A4 landscape/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('blendet UI-Chrome im Druck aus (screen-only Regel)', () => {
|
||||||
|
expect(printBlock).toContain('.screen-only');
|
||||||
|
expect(printBlock).toMatch(/\.screen-only\s*\{[^}]*display:\s*none/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Canvas/Druckbereich füllt die Seite (print-area Regel)', () => {
|
||||||
|
expect(printBlock).toContain('.print-area');
|
||||||
|
expect(printBlock).toMatch(/\.print-area\s*\{[^}]*width:\s*100%/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('print-layout-Modus erzwingt Farbdruck-Hintergründe (print-color-adjust)', () => {
|
||||||
|
expect(printBlock).toMatch(/-webkit-print-color-adjust:\s*exact/);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user