diff --git a/src/main/bundles/dev.bundle b/src/main/bundles/dev.bundle index 60bc3fe..8d42cce 100644 Binary files a/src/main/bundles/dev.bundle and b/src/main/bundles/dev.bundle differ diff --git a/src/main/frontend/styles/pdf-builder.css b/src/main/frontend/styles/pdf-builder.css index 1478843..650a077 100644 --- a/src/main/frontend/styles/pdf-builder.css +++ b/src/main/frontend/styles/pdf-builder.css @@ -52,6 +52,35 @@ justify-content: center; } +/* Inspector (right side) */ +.pdf-inspector { + width: 300px; + min-width: 260px; + border: 1px solid var(--lumo-contrast-20pct); + border-radius: var(--lumo-border-radius-m); + background: var(--lumo-base-color); + box-shadow: var(--lumo-box-shadow-xs); + padding: var(--lumo-space-m); + max-height: calc(100vh - 220px); + overflow: auto; +} + +.pdf-inspector .inspector-title { + font-weight: 600; + margin-bottom: var(--lumo-space-s); +} + +.inspector-item { + display: flex; + align-items: center; + gap: var(--lumo-space-s); + padding: var(--lumo-space-xs) 0; + border-bottom: 1px dashed var(--lumo-contrast-10pct); +} +.inspector-item:last-child { border-bottom: none; } +.inspector-item > span { min-width: 90px; font-size: var(--lumo-font-size-s); color: var(--lumo-secondary-text-color); } +.pdf-inspector vaadin-number-field { width: 120px; } + .pdf-canvas { width: 60%; aspect-ratio: 210 / 297; /* A4 */ @@ -128,6 +157,12 @@ display: none !important; } +/* Export-only: make box backgrounds transparent */ +.pdf-canvas.capturing-export .canvas-frame, +.pdf-canvas.capturing-export .text-editor { + background: transparent !important; +} + /* Image frames: no scrollbars */ .image-frame { overflow: hidden; /* hide scrollbars */ diff --git a/src/main/java/de/assecutor/votianlt/pages/view/PDFBuilderView.java b/src/main/java/de/assecutor/votianlt/pages/view/PDFBuilderView.java index 659d326..0466fd0 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/PDFBuilderView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/PDFBuilderView.java @@ -5,7 +5,6 @@ import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.dependency.CssImport; import com.vaadin.flow.component.dialog.Dialog; -import com.vaadin.flow.component.html.Anchor; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.H3; import com.vaadin.flow.component.html.Image; @@ -38,6 +37,10 @@ import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Objects; import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.component.textfield.NumberField; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; @Route(value = "pdf-builder", layout = MainLayout.class) @PageTitle("PDF Builder") @@ -48,7 +51,8 @@ public class PDFBuilderView extends Div { private final PDFGenerationService pdfService; private final PdfTemplateService templateService; - private final PdfCanvas canvas; + private final ObjectMapper objectMapper = new ObjectMapper(); + private VerticalLayout inspectorList; public PDFBuilderView(PDFGenerationService pdfService, PdfTemplateService templateService) { this.pdfService = pdfService; @@ -61,9 +65,10 @@ public class PDFBuilderView extends Div { workspace.addClassName("pdf-builder-workspace"); Div palette = createPalette(); - canvas = new PdfCanvas(); + PdfCanvas canvas = new PdfCanvas(); + Div inspector = createInspector(); - workspace.add(palette, canvas); + workspace.add(palette, canvas, inspector); add(workspace); // Aktionen unten: Speichern @@ -74,6 +79,9 @@ public class PDFBuilderView extends Div { save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); actions.add(save); add(actions); + + // initialer Refresh (leer zu Beginn) + refreshInspectorFromDom(); } private void openSaveDialog() { @@ -116,34 +124,132 @@ public class PDFBuilderView extends Div { } private String getCollectStateJs() { - return "(function(){\n" + + return "(function(){" + " const root = document.getElementById('pdf-canvas');\n" + " if(!root){ return JSON.stringify({items:[]}); }\n" + " root.classList.add('capturing');\n" + " try {\n" + - " const rootRect = root.getBoundingClientRect();\n" + - " const items = Array.from(root.querySelectorAll('.canvas-frame')).map(f=>{\n" + - " const r = f.getBoundingClientRect();\n" + - " const x = Math.round(r.left - rootRect.left);\n" + - " const y = Math.round(r.top - rootRect.top);\n" + - " const width = Math.round(r.width);\n" + - " const height = Math.round(r.height);\n" + + " const rect = root.getBoundingClientRect();\n" + + " const items = Array.from(root.querySelectorAll('.canvas-frame')).map((f, idx)=>{\n" + + " const csf = getComputedStyle(f);\n" + + " const x = Math.round(parseFloat(csf.left) || 0);\n" + + " const y = Math.round(parseFloat(csf.top) || 0);\n" + + " const width = Math.round(parseFloat(csf.width) || f.getBoundingClientRect().width);\n" + + " const height = Math.round(parseFloat(csf.height) || f.getBoundingClientRect().height);\n" + + " const z = (function(){ const zc = parseInt(csf.zIndex); return isNaN(zc)? idx : zc; })();\n" + " const img = f.querySelector('img');\n" + " if(img){\n" + - " return {type:'image', x, y, width, height, dataUrl: img.src};\n" + + " return {type:'image', x, y, width, height, z, dataUrl: img.src};\n" + " } else {\n" + " const editor = f.querySelector('.text-editor');\n" + " const cs = editor ? getComputedStyle(editor) : null;\n" + - " return {type:'text', x, y, width, height, html: editor?editor.innerHTML:'', style: cs?{fontSize: cs.fontSize, color: cs.color}:{}};\n" + + " return {type:'text', x, y, width, height, z, html: editor?editor.innerHTML:'', editorStyle: cs?{fontSize: cs.fontSize, color: cs.color, lineHeight: cs.lineHeight}:{}};\n" + " }\n" + " });\n" + - " return JSON.stringify({items});\n" + + " return JSON.stringify({canvas:{width: Math.round(rect.width), height: Math.round(rect.height)}, items});\n" + " } finally {\n" + " root.classList.remove('capturing');\n" + " }\n" + "})()"; } + private Div createInspector() { + Div inspector = new Div(); + inspector.addClassName("pdf-inspector"); + H3 title = new H3("Elemente auf Canvas"); + title.addClassName("inspector-title"); + inspectorList = new VerticalLayout(); + inspectorList.setPadding(false); + inspectorList.setSpacing(false); + inspector.add(title, inspectorList); + Button refresh = new Button("Aktualisieren", e -> refreshInspectorFromDom()); + refresh.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + inspector.add(refresh); + return inspector; + } + + private void refreshInspectorFromDom() { + String js = getFramesInspectorJs(); + UI.getCurrent().getPage().executeJs(js).then(String.class, json -> { + try { + String safeJson = (json == null || json.isBlank()) ? "[]" : json; + List frames = objectMapper.readValue(safeJson, new TypeReference>(){}); + renderInspectorItems(frames); + } catch (Exception ex) { + Notification.show("Inspector-Update fehlgeschlagen: " + ex.getMessage(), 3000, Notification.Position.MIDDLE); + } + }); + } + + private void renderInspectorItems(List frames) { + inspectorList.removeAll(); + if (frames == null || frames.isEmpty()) return; + for (FrameInfo f : frames) { + Div row = new Div(); + row.addClassName("inspector-item"); + Span label = new Span(("text".equals(f.type) ? "Text" : "Bild") + " (" + f.id + ")"); + NumberField x = makeField("X", f.x); + NumberField y = makeField("Y", f.y); + NumberField w = makeField("Breite", f.width); + NumberField h = makeField("Höhe", f.height); + // Update handler (nur Client-Änderungen) + x.addValueChangeListener(e -> { if (e.isFromClient()) setFrameRect(f.id, x.getValue(), y.getValue(), w.getValue(), h.getValue()); }); + y.addValueChangeListener(e -> { if (e.isFromClient()) setFrameRect(f.id, x.getValue(), y.getValue(), w.getValue(), h.getValue()); }); + w.addValueChangeListener(e -> { if (e.isFromClient()) setFrameRect(f.id, x.getValue(), y.getValue(), w.getValue(), h.getValue()); }); + h.addValueChangeListener(e -> { if (e.isFromClient()) setFrameRect(f.id, x.getValue(), y.getValue(), w.getValue(), h.getValue()); }); + row.add(label, x, y, w, h); + inspectorList.add(row); + } + } + + private NumberField makeField(String label, Integer value){ + NumberField nf = new NumberField(label); + nf.setStep(1d); + nf.setMin(0d); + if (value != null) { nf.setValue(value.doubleValue()); } + nf.setWidth("120px"); + return nf; + } + + private void setFrameRect(String id, Double x, Double y, Double w, Double h){ + if (id == null || id.isBlank()) return; + double dx = x == null ? 0 : Math.max(0, x); + double dy = y == null ? 0 : Math.max(0, y); + double dw = w == null ? 10 : Math.max(10, w); + double dh = h == null ? 10 : Math.max(10, h); + String js = "(function(){ const el = document.getElementById('" + id + "'); if(!el) return; el.style.left='" + (int)dx + "px'; el.style.top='" + (int)dy + "px'; el.style.width='" + (int)dw + "px'; el.style.height='" + (int)dh + "px'; })()"; + UI.getCurrent().getPage().executeJs(js); + // Danach Inspector neu laden + refreshInspectorFromDom(); + } + + private String getFramesInspectorJs(){ + return "(function(){\n" + + " const root = document.getElementById('pdf-canvas');\n" + + " if(!root){ return '[]'; }\n" + + " const items = Array.from(root.querySelectorAll('.canvas-frame')).map(f=>{\n" + + " const cs = getComputedStyle(f);\n" + + " const id = f.id || f.getAttribute('data-frame-id') || '';\n" + + " const type = f.classList.contains('text-frame') ? 'text' : 'image';\n" + + " const x = Math.round(parseFloat(cs.left) || 0);\n" + + " const y = Math.round(parseFloat(cs.top) || 0);\n" + + " const width = Math.round(parseFloat(cs.width) || f.getBoundingClientRect().width);\n" + + " const height = Math.round(parseFloat(cs.height) || f.getBoundingClientRect().height);\n" + + " return {id, type, x, y, width, height};\n" + + " });\n" + + " return JSON.stringify(items);\n" + + "})()"; + } + + private static class FrameInfo { + public String id; + public String type; + public Integer x; + public Integer y; + public Integer width; + public Integer height; + } + private Div createPalette() { Div palette = new Div(); palette.addClassName("pdf-palette"); @@ -189,16 +295,16 @@ public class PDFBuilderView extends Div { private void exportCanvasToPdf(String templateName) { // html2canvas laden (falls nicht vorhanden) und INNERES Canvas erfassen String js = "const el = document.getElementById('pdf-canvas');\n" + - "el.classList.add('capturing');\n" + + "el.classList.add('capturing','capturing-export');\n" + "const capture = () => html2canvas(el).then(c => c.toDataURL('image/png'));\n" + "if (window.html2canvas) {\n" + - " return capture().then(r => { el.classList.remove('capturing'); return r; }).catch(err => { el.classList.remove('capturing'); return null; });\n" + + " return capture().then(r => { el.classList.remove('capturing','capturing-export'); return r; }).catch(err => { el.classList.remove('capturing','capturing-export'); return null; });\n" + "}\n" + "return new Promise(resolve => {\n" + " const s = document.createElement('script');\n" + " s.src = 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js';\n" + - " s.onload = () => capture().then(r => { el.classList.remove('capturing'); resolve(r); }).catch(() => { el.classList.remove('capturing'); resolve(null); });\n" + - " s.onerror = () => { el.classList.remove('capturing'); resolve(null); };\n" + + " s.onload = () => capture().then(r => { el.classList.remove('capturing','capturing-export'); resolve(r); }).catch(() => { el.classList.remove('capturing','capturing-export'); resolve(null); });\n" + + " s.onerror = () => { el.classList.remove('capturing','capturing-export'); resolve(null); };\n" + " document.head.appendChild(s);\n" + "});"; @@ -236,11 +342,12 @@ public class PDFBuilderView extends Div { } // Canvas-Komponente mit Drop-Handling und Frame-Erzeugung - static class PdfCanvas extends Div { + class PdfCanvas extends Div { @Serial private static final long serialVersionUID = 1L; private final Div inner; + private int frameCounter = 0; PdfCanvas() { addClassName("pdf-canvas-wrapper"); @@ -258,7 +365,7 @@ public class PDFBuilderView extends Div { private void enableDrop(Div target) { // Dragover/Drops auf dem Canvas erfassen und an Server melden target.getElement().executeJs( - "const canvas = this;\n" + + "const canvas = this;" + "canvas.addEventListener('dragover', e => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; });\n" + "canvas.addEventListener('drop', e => {\n" + " e.preventDefault();\n" + @@ -300,10 +407,20 @@ public class PDFBuilderView extends Div { } } + @com.vaadin.flow.component.ClientCallable + private void onFrameChanged() { + // Nach Drag/Resize: Inspector-Seite aktualisieren + PDFBuilderView.this.refreshInspectorFromDom(); + } + private void addTextFrame(int x, int y) { Div frame = new Div(); frame.addClassName("canvas-frame"); frame.addClassName("text-frame"); + String id = "frame-" + (++frameCounter); + frame.setId(id); + frame.getElement().setAttribute("data-frame-id", id); + frame.getElement().setAttribute("data-type", "text"); frame.getStyle().set("left", x + "px"); frame.getStyle().set("top", y + "px"); frame.getStyle().set("width", "300px"); @@ -364,6 +481,8 @@ public class PDFBuilderView extends Div { enableFrameDragging(frame); // Aktivierungsverhalten enableFrameActivation(frame); + // Inspector aktualisieren + PDFBuilderView.this.refreshInspectorFromDom(); } private void promptImageUpload(int x, int y) { @@ -396,6 +515,10 @@ public class PDFBuilderView extends Div { Div frame = new Div(); frame.addClassName("canvas-frame"); frame.addClassName("image-frame"); + String id = "frame-" + (++frameCounter); + frame.setId(id); + frame.getElement().setAttribute("data-frame-id", id); + frame.getElement().setAttribute("data-type", "image"); frame.getStyle().set("left", x + "px"); frame.getStyle().set("top", y + "px"); frame.getStyle().set("width", "260px"); @@ -419,12 +542,15 @@ public class PDFBuilderView extends Div { enableFrameDragging(frame); // Aktivierungsverhalten enableFrameActivation(frame); + // Inspector aktualisieren + PDFBuilderView.this.refreshInspectorFromDom(); } private void enableFrameDragging(Div frame) { frame.getElement().executeJs( "const frame = this;\n" + "const root = document.getElementById('pdf-canvas');\n" + + "const canvas = root;\n" + "let startX = 0, startY = 0, startLeft = 0, startTop = 0;\n" + "frame.__dragMoved = false;\n" + "frame.__downOutsideEditor = false;\n" + @@ -478,11 +604,15 @@ public class PDFBuilderView extends Div { " document.removeEventListener('touchmove', onMove);\n" + " // Plain Click auf Frame (nicht im Editor, kein Drag): Editor fokussieren\n" + " if (!frame.__dragMoved && frame.__downOutsideEditor){ const ed = frame.querySelector('.text-editor'); if (ed){ ed.focus(); placeCaretAtEnd(ed); } }\n" + + " // Inspector informieren\n" + + " try { if (canvas && canvas.parentElement && canvas.parentElement.$server && canvas.parentElement.$server.onFrameChanged) { canvas.parentElement.$server.onFrameChanged(); } } catch(e){}\n" + " // dragMoved-Flag nach dem Click-Zyklus zurücksetzen\n" + " setTimeout(()=>{ frame.__dragMoved = false; frame.__downOutsideEditor = false; }, 0);\n" + "}\n" + "frame.addEventListener('mousedown', onDown);\n" + - "frame.addEventListener('touchstart', onDown, { passive: false });\n" + "frame.addEventListener('touchstart', onDown, { passive: false });\n" + + "// Größenänderungen beobachten (ResizeObserver)\n" + + "if (window.ResizeObserver) { const ro = new ResizeObserver(()=>{ try { if (canvas && canvas.parentElement && canvas.parentElement.$server && canvas.parentElement.$server.onFrameChanged) { canvas.parentElement.$server.onFrameChanged(); } } catch(e){} }); ro.observe(frame); }\n" ); }